Skip to content

Accidental fallback match for Collection-type beans due to @Bean-level qualifier annotation #35908

@dominikbrandon

Description

@dominikbrandon

Problem statement

Between spring-beans 6.2.12 and 6.2.14 something changed when it comes to resolving Collection-type beans. I doubt it was an intentional change as it may break many projects so I thought it's reasonable to report the issue and get the clarification from your team. Could you clarify if the change in behaviour is expected?

See more details on the issue and pointing out the possible culprit below.

Elaboration on the issue

When the following 2 libraries are on the classpath, List<BulkheadConfigCustomizer> argument resolves to List<Worker> which is wrong and results in a ClassCastException (see stacktrace below).

io.github.resilience4j:resilience4j-spring-boot3:2.2.0

public abstract class AbstractBulkheadConfigurationOnMissingBean {
...
@Bean
@ConditionalOnMissingBean(name = "compositeBulkheadCustomizer")
@Qualifier("compositeBulkheadCustomizer")
public CompositeCustomizer<BulkheadConfigCustomizer> compositeBulkheadCustomizer(
    @Autowired(required = false) List<BulkheadConfigCustomizer> customizers) {
    return new CompositeCustomizer<>(customizers);
}

io.temporal:temporal-spring-boot-autoconfigure:1.30.1

public class RootNamespaceAutoConfiguration {
...
@Primary
@Bean(name = "temporalWorkers")
@Conditional(WorkersPresentCondition.class)
public Collection<Worker> workers(
    @Qualifier("temporalWorkersTemplate") WorkersTemplate workersTemplate) {
  Collection<Worker> workers = workersTemplate.getWorkers();
  workers.forEach(
      worker -> beanFactory.registerSingleton("temporalWorker-" + worker.getTaskQueue(), worker));
  return workers;
}

Stacktrace:

Caused by: java.lang.ClassCastException: class io.temporal.worker.Worker cannot be cast to class io.github.resilience4j.common.CustomizerWithName (io.temporal.worker.Worker and io.github.resilience4j.common.CustomizerWithName are in unnamed module of loader 'app')
at java.base/java.util.ArrayList.forEach(ArrayList.java:1604)
at io.github.resilience4j.common.CompositeCustomizer.(CompositeCustomizer.java:34)
at io.github.resilience4j.springboot3.bulkhead.autoconfigure.AbstractBulkheadConfigurationOnMissingBean.compositeBulkheadCustomizer(AbstractBulkheadConfigurationOnMissingBean.java:67)
at io.github.resilience4j.springboot3.bulkhead.autoconfigure.BulkheadConfigurationOnMissingBean$$SpringCGLIB$$0.CGLIB$compositeBulkheadCustomizer$8()
at io.github.resilience4j.springboot3.bulkhead.autoconfigure.BulkheadConfigurationOnMissingBean$$SpringCGLIB$$FastClass$$1.invoke()
at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:258)
at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:400)
at io.github.resilience4j.springboot3.bulkhead.autoconfigure.BulkheadConfigurationOnMissingBean$$SpringCGLIB$$0.compositeBulkheadCustomizer()
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.lambda$instantiate$0(SimpleInstantiationStrategy.java:172)
... 49 more

Tracing down the culprit

spring-beans:6.2.12

public class QualifierAnnotationAutowireCandidateResolver extends GenericTypeAwareAutowireCandidateResolver {
...
@Override
public boolean hasQualifier(DependencyDescriptor descriptor) {
	for (Annotation annotation : descriptor.getAnnotations()) {
		if (isQualifier(annotation.annotationType())) {
			return true;
		}
	}
	return false; // <--- RETURNS FALSE, NO BEAN IS RESOLVED
}

spring-beans:6.2.14

@Override
public boolean hasQualifier(DependencyDescriptor descriptor) {
	for (Annotation annotation : descriptor.getAnnotations()) {
		if (isQualifier(annotation.annotationType())) {
			return true;
		}
	}
	MethodParameter methodParam = descriptor.getMethodParameter(); // compositeBulkheadCustomizer
	if (methodParam != null) {
		for (Annotation annotation : methodParam.getMethodAnnotations()) { // @Qualifier("compositeBulkheadCustomizer")
			if (isQualifier(annotation.annotationType())) {
				return true; // <--- RETURNS TRUE, CAUSES A List PRESENT IN THE CONTEXT TO BE RESOLVED TO List<SomeSpecificType>
			}
		}
	}
	return false;
}

I don't see why @Qualifier annotation present on compositeBulkheadCustomizer method would be supposed to mean "yes we accept any List present in the Spring context as List<BulkheadConfigCustomizer> argument".

Metadata

Metadata

Assignees

Labels

in: coreIssues in core modules (aop, beans, core, context, expression)status: backportedAn issue that has been backported to maintenance branchestype: regressionA bug that is also a regression

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions