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
@Configuration
or@Component
onConfig
@Configuration
// @Component
class Config extends BaseConfig {
}
class BaseConfig {
@Bean
String bean1() {
return "bean1";
}
@Bean
String bean2() {
return "bean2";
}
}
- use
@Import
instead 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
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.
@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
@Import
does.
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
@ContextConfiguration
behaves 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
@Import
does.
Agreed, if the semantics between class registration and @Import
are different it is rather unexpected.
Comment From: sbrannen
Fix inconsistency between
@ContextConfiguration
and@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
@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
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-context
this 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
@Import
works 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
@Bean
methods 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.
@ContextConfiguration
refers 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 whenClass
references 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.