I'm not sure it's a bug or undocumented limitation.
If no @Bean defined in Config then @Bean defined in BaseConfig will be ignored.
@ContextConfiguration(classes = Config.class)
@ExtendWith(SpringExtension.class)
public class InjectionTests {
@Autowired
private String bean1;
@Autowired
private String bean2;
@Test
void test() {
assertThat(bean1).isEqualTo("bean1");
assertThat(bean2).isEqualTo("bean2");
}
}
class Config extends BaseConfig {
}
class BaseConfig {
@Bean
String bean1() {
return "bean1";
}
@Bean
String bean2() {
return "bean2";
}
}
there are many workarounds
1. move any one of @Bean to Config
class Config extends BaseConfig {
@Bean
String bean2() {
return "bean2";
}
}
class BaseConfig {
@Bean
String bean1() {
return "bean1";
}
}
- mark
@Configurationor@ComponentonConfig
@Configuration
// @Component
class Config extends BaseConfig {
}
class BaseConfig {
@Bean
String bean1() {
return "bean1";
}
@Bean
String bean2() {
return "bean2";
}
}
- use
@Importinstead ofContextConfiguration
@Import(Config.class)
@ExtendWith(SpringExtension.class)
public class InjectionTests {
@Autowired
private String bean1;
@Autowired
private String bean2;
@Test
void test() {
assertThat(bean1).isEqualTo("bean1");
assertThat(bean2).isEqualTo("bean2");
}
}
Comment From: snicoll
@quaff neither? This has nothing to do with ContextConfiguration as far as I can see. 2. is what you should be doing all along if you want that class to be processed as a configuration class.
Comment From: quaff
@quaff neither? This has nothing to do with
ContextConfigurationas far as I can see. 2. is what you should be doing all along if you want that class to be processed as a configuration class.
@snicoll I know option 2 is recommended, I think there is something arguable here. Can plain class be imported as configuration class? If no, then workaround 1 and 3 should be prohibited since they are supported by accident. If yes, then the inconsistency should be fixed.
Comment From: snicoll
I know option 2 is recommended
That's not a recommendation. The stereotype must be applied, otherwise the context is using a fallback called "lite mode".
Can plain class be imported as configuration class?
Yes and it's then treated with "lite mode", see https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-java-basic-concepts
Comment From: quaff
I know option 2 is recommended
That's not a recommendation. The stereotype must be applied, otherwise the context is using a fallback called "lite mode".
Can plain class be imported as configuration class?
Yes and it's then treated with "lite mode", see https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-java-basic-concepts
AFAIK, the lite mode only affects inter-bean dependencies, that's said it should equals to @Configuration(proxyBeanMethods = false) or @Component, with my case I need move one of @Bean from super class to child class, then It act like the lite mode, does this restriction is expected and documented?
Comment From: snicoll
Sorry @quaff, I thought this was one property of lite mode but that doesn't seem to be the case. Maybe @sbrannen knows why @ContextConfiguration behaves this way?
Comment From: sbrannen
I don't believe this is specific to @ContextConfiguration.
Rather, I believe the described behavior is due to the semantics of ConfigurationClassUtils.isConfigurationCandidate(AnnotationMetadata) which delegates to hasBeanMethods(AnnotationMetadata) which (AFAICT by inspection) does not search for @Bean methods in super types.
But that's just an assumption. I'd have to create tests to verify/debug it.
Comment From: quaff
This method doesn't check super class has bean methods, I'm trying to provide PR. https://github.com/spring-projects/spring-framework/blob/993a69d3a9fff202b731e69b4655535cd4647faa/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassUtils.java#L179-L189
Comment From: snicoll
Sam, your quote is outdated. I already explained I thought this was a "feature" of lite mode.
Whatever the underlying util this calls, it's very odd that it behaves differently than what @Import does.
Comment From: quaff
Sam, your quote is outdated. I already explained I thought this was a "feature" of lite mode.
Whatever the underlying util this calls, it's very odd that it behaves differently than what
@Importdoes.
Yes, It split to two issues.
1. Fix hasBeanMethods(AnnotationMetadata)
2. Fix inconsistency between @ContextConfiguration and @Import
Comment From: sbrannen
@snicoll, my quote is not outdated.
I was replying directly to your previous question:
Maybe @sbrannen knows why
@ContextConfigurationbehaves this way?
As I pointed out in https://github.com/spring-projects/spring-framework/issues/30449#issuecomment-1541512913, it's not @ContextConfiguration behaving in any special way.
Rather, it's standard behavior from spring-context.
Comment From: sbrannen
it's very odd that it behaves differently than what
@Importdoes.
Agreed, if the semantics between class registration and @Import are different it is rather unexpected.
Comment From: sbrannen
Fix inconsistency between
@ContextConfigurationand@Import
Again, this is not related to @ContextConfiguration or the TestContext framework.
Rather, this is the standard behavior for a Class registered directly with an ApplicationContext -- for example, using AnnotatedBeanDefinitionReader or AnnotationConfigApplicationContext.
For that use case, a non-annotated class must have a local @Bean method declared in order for the class to be processed using @Bean 'lite' mode.
In contrast, any class imported with @Import will be processed as a @Configuration class.
https://github.com/spring-projects/spring-framework/blob/993a69d3a9fff202b731e69b4655535cd4647faa/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java#L509-L515
This method doesn't check super class has bean methods, I'm trying to provide PR.
Technically speaking, changing that behavior would be a breaking change. So if we decide to do that, it would likely have to be in 6.1
@jhoeller, thoughts?
Comment From: quaff
@sbrannen I've submitted https://github.com/spring-projects/spring-framework/pull/30462
Comment From: quaff
In contrast, any class imported with
@Importwill be processed as a@Configurationclass.https://github.com/spring-projects/spring-framework/blob/993a69d3a9fff202b731e69b4655535cd4647faa/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java#L509-L515
It explain the difference and it's expected behavior, we should improve the document.
This method doesn't check super class has bean methods, I'm trying to provide PR.
Technically speaking, changing that behavior would be a breaking change. So if we decide to do that, it would likely have to be in 6.1
IMO it's a fix and should be backported to 5.x also.
Comment From: sbrannen
- Superseded by #30462
Comment From: snicoll
@snicoll, my quote is not outdated.
It wasn't a quote so it can't be. Sorry, I thought you were quoting me.
The PR that supersedes this issue has been closed and I agree with that but I still wonder if any other user-facing API ends up using the code in spring-context this way.
What I meant with outdated is that I now think this is a problem with @ContextConfiguration. If @Import works differently, this doesn't look right as they are supposed to mean the same thing, don't they? Juergen mentioned that we can't crawl the parent for @Bean methods and that makes sense if you're working at the ASM level but that's not the case here. @ContextConfiguration refers to classes, just like @Import. Can we please revisit this?
Comment From: sbrannen
@snicoll, my quote is not outdated.
It wasn't a quote so it can't be. Sorry, I thought you were quoting me.
Ahhh, OK. I understand the confusion now. Thanks for clarifying. 👍
The PR that supersedes this issue has been closed and I agree with that
I only partially agree with that, which I will elaborate on below.
but I still wonder if any other user-facing API ends up using the code in
spring-contextthis way.
Yes, this is the "standard" behavior for "annotated classes" that are registered directly with an ApplicationContext.
For example, the following demonstrates the same undesired behavior without using any part of the TestContext framework.
class BeanLiteModeTests {
@Test
void test() {
try (var context = new AnnotationConfigApplicationContext(Config.class)) {
var bean1 = context.getBean("bean1", String.class);
var bean2 = context.getBean("bean2", String.class);
assertThat(bean1).isEqualTo("bean1");
assertThat(bean2).isEqualTo("bean2");
}
}
// @org.springframework.stereotype.Component
static class Config extends BaseConfig {
}
static class BaseConfig {
@Bean
String bean1() {
return "bean1";
}
@Bean
String bean2() {
return "bean2";
}
}
}
If
@Importworks differently, this doesn't look right as they are supposed to mean the same thing, don't they?
Yes, I agree: @ContextConfiguration and @Import should ideally provide the same semantics.
What I meant with outdated is that I now think this is a problem with
@ContextConfiguration.
Based on the example I provided above, I would say it's an issue with "registering classes directly with an application context" in general.
Juergen mentioned that we can't crawl the parent for
@Beanmethods and that makes sense if you're working at the ASM level but that's not the case here.
Right. If the classes are picked up via component scanning, I agree that we should not scan the class hierarchy for @Bean methods.
@ContextConfigurationrefers to classes, just like@Import.
That's the key differentiator!
Any time the user supplies Class references directly, I think the semantics should be the same as when classes are supplied via @Import.
In other words, whenever a Class is registered with an ApplicationContext, I think it would make sense to process the class "as a configuration class" just like the code path for @Import.
If we made a change along those lines, then AnnotatedBeanDefinitionReader, AnnotationConfigApplicationContext(Class<?>...), and @ContextConfiguration would have the same semantics as @Import (with regard to @Bean lite mode).
Can we please revisit this?
Yes, thanks for reopening the issue.
Comment From: sbrannen
@jhoeller, what do you think about having consistent semantics for @Bean 'lite' mode when Class references are registered directly with an ApplicationContext?
I suppose the alternative would be to change the behavior of @ContextConfiguration so that classes are registered with @Import semantics (if feasible), but I believe that would require changes in Spring Boot and any other custom SmartContextLoader implementations.
Comment From: sbrannen
@jhoeller, what do you think about having consistent semantics for
@Bean'lite' mode whenClassreferences are registered directly with anApplicationContext?
Update: I've prototyped an implementation for this which treats any Class registered via AnnotatedBeanDefinitionReader as a @Configuration class candidate with @Bean 'lite' mode semantics.
https://github.com/spring-projects/spring-framework/compare/main...sbrannen:spring-framework:issues/gh-30449-bean-lite-mode-for-registered-classes
Comment From: sbrannen
This has been addressed in commit a4553171222370e4e526bd5d6c6c019fe54c0f39 and can be tested in upcoming snapshots for 6.0.10.