Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@Bean 'lite' mode not supported if @Bean methods are not declared locally #30449

Closed
quaff opened this issue May 9, 2023 · 20 comments
Closed
Assignees
Labels
in: core Issues in core modules (aop, beans, core, context, expression) type: enhancement A general enhancement
Milestone

Comments

@quaff
Copy link
Contributor

quaff commented May 9, 2023

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");
	}

}
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label May 9, 2023
@snicoll
Copy link
Member

snicoll commented May 9, 2023

@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 snicoll closed this as not planned Won't fix, can't repro, duplicate, stale May 9, 2023
@snicoll snicoll added status: invalid An issue that we don't feel is valid and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels May 9, 2023
@quaff
Copy link
Contributor Author

quaff commented May 10, 2023

@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.

@snicoll
Copy link
Member

snicoll commented May 10, 2023

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

@quaff
Copy link
Contributor Author

quaff commented May 10, 2023

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?

@snicoll snicoll reopened this May 10, 2023
@snicoll
Copy link
Member

snicoll commented May 10, 2023

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?

@snicoll snicoll added status: waiting-for-triage An issue we've not yet triaged or decided on and removed status: invalid An issue that we don't feel is valid labels May 10, 2023
@sbrannen
Copy link
Member

sbrannen commented May 10, 2023

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.

@quaff
Copy link
Contributor Author

quaff commented May 10, 2023

This method doesn't check super class has bean methods, I'm trying to provide PR.

static boolean hasBeanMethods(AnnotationMetadata metadata) {
try {
return metadata.hasAnnotatedMethods(Bean.class.getName());
}
catch (Throwable ex) {
if (logger.isDebugEnabled()) {
logger.debug("Failed to introspect @Bean methods on class [" + metadata.getClassName() + "]: " + ex);
}
return false;
}
}

@snicoll
Copy link
Member

snicoll commented May 10, 2023

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.

@sbrannen sbrannen added the in: core Issues in core modules (aop, beans, core, context, expression) label May 10, 2023
@quaff
Copy link
Contributor Author

quaff commented May 10, 2023

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

@sbrannen
Copy link
Member

@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 #30449 (comment), it's not @ContextConfiguration behaving in any special way.

Rather, it's standard behavior from spring-context.

@sbrannen
Copy link
Member

sbrannen commented May 10, 2023

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.

@sbrannen sbrannen changed the title @ContextConfiguration doesn't work as expect if no @Bean declaration directly @Bean _lite_ mode not supported if @Bean methods are not declared locally May 10, 2023
@sbrannen sbrannen changed the title @Bean _lite_ mode not supported if @Bean methods are not declared locally @Bean 'lite' mode not supported if @Bean methods are not declared locally May 10, 2023
@sbrannen
Copy link
Member

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.

else {
// Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
// process it as an @Configuration class
this.importStack.registerImport(
currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter);
}

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?

@quaff
Copy link
Contributor Author

quaff commented May 10, 2023

@sbrannen I've submitted #30462

quaff added a commit to quaff/spring-framework that referenced this issue May 10, 2023
@quaff
Copy link
Contributor Author

quaff commented May 10, 2023

In contrast, any class imported with @Import will be processed as a @Configuration class.

else {
// Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
// process it as an @Configuration class
this.importStack.registerImport(
currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter);
}

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.

quaff added a commit to quaff/spring-framework that referenced this issue May 11, 2023
quaff added a commit to quaff/spring-framework that referenced this issue May 15, 2023
…te mode

ConfigurationClassUtils should check bean methods in super classes also

Fix spring-projectsGH-30449
@sbrannen
Copy link
Member

@sbrannen sbrannen closed this as not planned Won't fix, can't repro, duplicate, stale May 15, 2023
@sbrannen sbrannen added status: superseded An issue that has been superseded by another and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels May 15, 2023
@snicoll
Copy link
Member

snicoll commented May 16, 2023

@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?

@snicoll snicoll reopened this May 16, 2023
@snicoll snicoll added status: waiting-for-triage An issue we've not yet triaged or decided on and removed status: superseded An issue that has been superseded by another labels May 16, 2023
@sbrannen
Copy link
Member

sbrannen commented May 16, 2023

@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.

@sbrannen
Copy link
Member

sbrannen commented May 16, 2023

@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.

@sbrannen sbrannen assigned sbrannen and jhoeller and unassigned jhoeller May 22, 2023
@sbrannen sbrannen added this to the 6.0.10 milestone May 22, 2023
@sbrannen sbrannen added type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels May 22, 2023
@sbrannen
Copy link
Member

sbrannen commented May 22, 2023

@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.

main...sbrannen:spring-framework:issues/gh-30449-bean-lite-mode-for-registered-classes

sbrannen added a commit to sbrannen/spring-framework that referenced this issue May 23, 2023
@sbrannen
Copy link
Member

This has been addressed in commit a455317 and can be tested in upcoming snapshots for 6.0.10.

mdeinum pushed a commit to mdeinum/spring-framework that referenced this issue Jun 29, 2023
Prior to this commit, if a non-annotated [1] class was registered
directly with an ApplicationContext -- for example, via
AnnotatedBeanDefinitionReader, AnnotationConfigApplicationContext, or
@ContextConfiguration -- it was not considered a configuration class in
'lite' mode unless @bean methods were declared locally. In other words,
if the registered class didn't declare local @bean methods but rather
extended a class that declared @bean methods, then the registered class
was not parsed as a @configuration class in 'lite' mode, and the @bean
methods were ignored.

Whereas, a non-annotated class registered via @import is always
considered to be a configuration class candidate.

To address this discrepancy between @import'ed classes and classes
registered directly with an ApplicationContext, this commit treats any
class registered via AnnotatedBeanDefinitionReader as a @configuration
class candidate with @bean 'lite' mode semantics.

[1] In this context, "non-annotated" means a class not annotated (or
meta-annotated) with @component, @componentscan, @import, or
@ImportResource.

Closes spring-projectsgh-30449
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: core Issues in core modules (aop, beans, core, context, expression) type: enhancement A general enhancement
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants