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";
    }
}
  1. mark @Configuration or @Component on Config
@Configuration
// @Component
class Config extends BaseConfig {

}

class BaseConfig {
    @Bean
    String bean1() {
        return "bean1";
    }
    @Bean
    String bean2() {
        return "bean2";
    }
}
  1. use @Import instead of ContextConfiguration
@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 when Class references are registered directly with an ApplicationContext?

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.