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

BeanFactory.get*ForAnnotation should take @Bean metadata into account #22541

Closed
mdeinum opened this issue Mar 7, 2019 · 11 comments
Closed

BeanFactory.get*ForAnnotation should take @Bean metadata into account #22541

mdeinum opened this issue Mar 7, 2019 · 11 comments
Assignees
Labels
in: core Issues in core modules (aop, beans, core, context, expression) type: enhancement A general enhancement
Milestone

Comments

@mdeinum
Copy link
Contributor

mdeinum commented Mar 7, 2019

I'm currently writing a small BeanFactoryPostProcessor and for that I need to process annotations on @Bean methods. Or at least detect the bean definition based on that metadata. I was expecting that getBeanNamesForAnnotation (and friends) would also take annotations on @Bean methods into consideration. However it only works for annotated types.

So when doing something like this.

@Component
@CustomAnnotation
public class MyClass { ... }

It works. But doing the same on an @Bean annotated method doesn't work.

@Bean
@CustomAnnotation
public MyClass myClass() { ... }

It would be nice if the methods would return beans (or bean definitions) annotated throuhg @Bean methods as well. Especially if we want to apply this to beans that aren't under our own control (i.e. external libraries).

For the Spring annotations like @Lazy, @Primary etc. this already works but those are handled in specialized cases when parsing the configuration files. @Lazy on a component works the same as @Lazy on a @Bean method.

In this particulair case I wanted to detect beans with a special annotation so that we could register them in JNDI. It works when annotating the component with our specialized annotation not when using the annotation on @Bean methods. This is especially annoying for classes that are outside of our control.

Currently the only way is to extend the class only for adding the annotation.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Mar 7, 2019
@sbrannen
Copy link
Member

sbrannen commented Mar 7, 2019

Are you referring explicitly to getBeanNamesForAnnotation(...) and getBeansWithAnnotation(...) in org.springframework.beans.factory.ListableBeanFactory, or did you have other concrete methods in mind?

Also, if we were to introduce such support, would the introduction of a new method specific to your proposed use case be sufficient/acceptable?

@sbrannen sbrannen added in: core Issues in core modules (aop, beans, core, context, expression) type: enhancement A general enhancement status: waiting-for-feedback We need additional information before we can continue and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Mar 7, 2019
@mdeinum
Copy link
Contributor Author

mdeinum commented Mar 7, 2019

I'm indeed referring to those methods (I also believe they eventually delegate to the same internal methods).

I would not expect additional methods to be added, but if that is more convenient (or less of a surprise to existing users) then I'm perfectly fine with that.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Mar 7, 2019
@sbrannen
Copy link
Member

sbrannen commented Mar 7, 2019

I'm indeed referring to those methods (I also believe they eventually delegate to the same internal methods).

Yes, getBeansWithAnnotation() delegates to getBeanNamesForAnnotation().

I would not expect additional methods to be added, but if that is more convenient (or less of a surprise to existing users) then I'm perfectly fine with that.

Aiming for the element of least surprise is exactly my intention here: changing the behavior of those existing methods would technically be a breaking change since such invocations would start to return things that they previously did not.

@sbrannen
Copy link
Member

sbrannen commented Mar 8, 2019

If you want to implement this on your own, it is technically possible. For example, the following works for @Bean methods that do not accept arguments.

public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

	@Override
	public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
		Class<? extends Annotation> annotationType = MyAnnotation.class;

		Arrays.stream(beanFactory.getBeanDefinitionNames())//
				.map(beanFactory::getBeanDefinition)//
				.filter(bd -> !bd.isAbstract())//
				.filter(bd -> bd.getFactoryMethodName() != null)//
				.filter(bd -> bd.getFactoryBeanName() != null)//
				.filter(bd -> isBeanMethodAnnotated(beanFactory, bd, annotationType))//
				.forEach(bd -> System.out.println("@Bean method " + bd.getFactoryMethodName()
						+ " is annotated with @" + annotationType.getSimpleName() + "."));
	}

	private boolean isBeanMethodAnnotated(ConfigurableListableBeanFactory beanFactory,
			BeanDefinition beanDefinition, Class<? extends Annotation> annotationType) {

		BeanDefinition factoryBeanDefinition = beanFactory.getBeanDefinition(beanDefinition.getFactoryBeanName());

		if (factoryBeanDefinition instanceof AbstractBeanDefinition) {
			AbstractBeanDefinition abd = (AbstractBeanDefinition) factoryBeanDefinition;
			if (abd.hasBeanClass()) {
				Class<?> factoryClass = ClassUtils.getUserClass(abd.getBeanClass());
				String factoryMethodName = beanDefinition.getFactoryMethodName();
				Method factoryMethod = ReflectionUtils.findMethod(factoryClass, factoryMethodName);
				return AnnotatedElementUtils.isAnnotated(factoryMethod, Bean.class)
						&& AnnotatedElementUtils.isAnnotated(factoryMethod, annotationType);
			}
		}

		return false;
	}

}

If you want to support @Bean methods that accept arguments, that should also be possible by processing the ConstructorArgumentValues in the BeanDefinition and then selecting the correct factory method by supplying the argument types to ReflectionUtils.findMethod(...). However, at that point the code starts to become cumbersome (more so than it already is).

In any case, the above serves as a proof of concept.

@sbrannen sbrannen added this to the 5.2 M1 milestone Mar 8, 2019
@sbrannen
Copy link
Member

sbrannen commented Mar 8, 2019

Tentatively slated for 5.2 M1 for further investigation and discussion

@sbrannen sbrannen added the status: pending-design-work Needs design work before any code can be developed label Mar 8, 2019
@sbrannen sbrannen self-assigned this Mar 8, 2019
@sbrannen
Copy link
Member

sbrannen commented Mar 8, 2019

@mdeinum, feel free to experiment with the above proof of concept in my branch.

@mdeinum
Copy link
Contributor Author

mdeinum commented Mar 12, 2019

Wouldn't it be nicer to detect the annotations while scanning/detecting beans and store them in the BeanDefinition as a cache? That way it doesn't really matter if an @Component class is annotated or if the metadata comes from an @Bean annotated method.

For me as a user (I know I might not be the average user :) ), it doesn't really matter if I specify metadata on the @Bean or the class/interface.

@rstoyanchev rstoyanchev removed the status: feedback-provided Feedback has been provided label Mar 12, 2019
@sbrannen
Copy link
Member

sbrannen added a commit to sbrannen/spring-framework that referenced this issue Mar 14, 2019
@sbrannen
Copy link
Member

sbrannen commented Mar 14, 2019

Update: due to changes in RootBeanDefinition introduced in #22420, I can now simplify my previous prototype as follows.

public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

	@Override
	public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
		Class<? extends Annotation> annotationType = MyAnnotation.class;

		findAnnotatedBeanDefinitions(beanFactory, annotationType)//
				.forEach(bd -> System.out.println("@Bean method " + bd.getFactoryMethodName()
						+ " is annotated with @" + annotationType.getSimpleName() + "."));
	}

	private List<BeanDefinition> findAnnotatedBeanDefinitions(ConfigurableListableBeanFactory beanFactory,
			Class<? extends Annotation> annotationType) {

		return Arrays.stream(beanFactory.getBeanDefinitionNames())//
				.map(beanFactory::getBeanDefinition)//
				.filter(bd -> !bd.isAbstract())//
				.filter(RootBeanDefinition.class::isInstance)//
				.map(RootBeanDefinition.class::cast)//
				.filter(bd -> isAnnotatedBeanMethod(bd, annotationType))//
				.collect(toList());
	}

	private boolean isAnnotatedBeanMethod(RootBeanDefinition beanDefinition,
			Class<? extends Annotation> annotationType) {

		Method factoryMethod = beanDefinition.getResolvedFactoryMethod();
		return factoryMethod != null && AnnotatedElementUtils.isAnnotated(factoryMethod, Bean.class)
				&& AnnotatedElementUtils.isAnnotated(factoryMethod, annotationType);
	}

}

sbrannen added a commit to sbrannen/spring-framework that referenced this issue Mar 14, 2019
This commit makes use of the new getResolvedFactoryMethod() method in
RootBeanDefinition.

See spring-projectsgh-22541
@jhoeller
Copy link
Contributor

jhoeller commented Apr 3, 2019

On review, I'm in favor of redefining getBeanNamesForAnnotation to detect annotations on @Bean methods as well. We can do that sort of change in 5.2 even if technically may break compatibility for some callers; I doubt it'll be the case though since we're just falling back to checking the factory method. It's hard to come up with a scenario where the bean class does not have a particular annotation but the @Bean method has that annotation type, and an existing call to getBeanNamesForAnnotation for that particular annotation does not want to see the latter.

As mentioned above, this fits nicely with the getResolvedFactoryMethod revision that came with #22420. It also fits nicely with our qualifier resolution algorithm which checks the factory method as well.

@jhoeller jhoeller self-assigned this Apr 3, 2019
@jhoeller jhoeller removed the status: pending-design-work Needs design work before any code can be developed label Apr 3, 2019
@sbrannen
Copy link
Member

sbrannen commented Apr 3, 2019

SGTM 👍

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

No branches or pull requests

5 participants