spring-framework
spring-framework copied to clipboard
Allow creation of Beans that cannot be autowired by type unless qualified
I'm seeing more and more problems with complex Spring / Spring Boot applications where beans created in some dependency interfere with the operation of either another dependency or our own code.
Dependencies like to declare Beans because it is convenient to inject them where needed in the dependency's own code. However, some of these beans really shouldn't be public and available for general use. By declaring a Bean, it becomes publicly available, even if given its own name or if it is declared with a Qualifier:
@Bean("myName") @MyQualifer
public MyBean myBean() { ... }
This bean can still be injected anywhere (when it is the only option) because Spring will match it by type:
@Autowired private MyBean bean1;
This is highly undesirable for types that are used commonly by multiple dependencies. Some of the biggest offenders are ObjectMapper
, ThreadPoolTaskScheduler
, etc. Beans like this often get configured in a specific way for a specific dependency and making them public (by simple declaring them) means that these beans can end up in completely unrelated parts of an application.
Spring's injection capabilities are very useful, even in libraries, but just like a library should be able to limit what API is publicly available, it should also be able to decide which of its beans are safe to use in external code (published) and which are not (kept private).
Qualifiers and Bean names are however insufficient due to Spring's matching by type -- a Bean intended for internal use only can still leak out and be used in external code. I therefore suggest allowing Beans to be declared with restrictions on how it can be matched:
@Bean(value = "myName", name-must-match = true) public MyBean myBean() { ... }
Or:
@Bean(must-be-qualified = true) @MyQualifier public MyBean myBean() { ... }
The first declaration would require the wiring site to specify the bean name:
@Autowired @Qualifer("myName") private MyBean bean1;
The second would require:
@Autowired @MyQualfier private MyBean bean1;
This would NOT match:
@Autowired private MyBean myBean;
In this fashion, library authors can keep certain beans private to the library by keeping the Qualifier private or by using an obscure bean name (use at your own risk in external code).
The suggested names and even the entire mechanism are just for illustration. I could imagine there being different annotations for this purpose: @NamedBean
-- bean that must match by name to be considered for injection, and @QualifiedBean
-- bean that must match all qualifiers to be considered.
@hjohn I requested this long time ago, and I was responded with a very enlightning answer by (was it Andy Wilkinson?). The best approach to fulfill this need is creating a custom domain object that acts as a wrapper. For instance, my need was usually @Autoconfiguring something like a Rest/Jms/KafkaTemplate or ObjectMapper, to be customized and used exclusively within the boundary of my domain, but forbidding for qualifying as @Primary in the rest of the Boot Autconfiguration ecosystem.
You can then expose a @Bean like
@Getter
@RequiredArgsConstructor
DomainRest/Kafka/JmsTemplate.java
private final DomainRest/Kafka/JmsTemplate domainTemplate;
You can even use Lombok's @Delegate if you want a 1-to-1 functionally equivalent decorator
This is exactly the same as defining a theoretical @Bean(strictQualifying = true) @Qualifier("strict-qualif-implementation") + with a @Autowired @Qualifier("strict-qualif-implementation") injection
Sorry, but I don't consider wrapping every bean that I don't want exposed a very good answer to this problem. A much better way to approach this is how CDI does this. Every bean without a qualifier silently gets a default qualifier added to it, and every injection point defined without a qualifier also gets this default qualifier. Now when you define a bean with your own qualifier, it will not match with an injection point that does not explicitly have that qualifier:
@Bean
A myBean(); // gets defined as "@Default A"
@Bean @Red
A myBean(); // gets defined as "@Red A"
@Bean @Default @Red
A myBean(); // gets defined as "@Default @Red A"
@Autowired A a; // accepts "@Default A" or "@Default @Red A" but not plain "@Red A"
@Autowired @Red A a; // accepts "@Red A" or "@Default @Red A" but not plain "@Default A"
Yes, its my same need. In a way, I can understand what I was told, i.e, that if you need to strict qualify Red A everywhere, it is not an A anymore, it is a RedA and must be injected as such. Kinda Liskov.
I've been bitten by this problem again in a multi module project. When upgrading a library, a bean which was clearly not intended to be used for normal use was injected into an injection point that just expected a default version of that bean.
I've taken the time to construct a solution which is somewhat similar to what CDI offers, but in order to remain backwards compatible with how Spring selects candidates by default (given the many projects out there) I've flipped the logic around. With the AutowireCandidateResolver
below it is possible to annotate a canditate with a NotDefault
annotation. This prevents the candidate from being injected into injection points without any annotations (or if it has annotations, they must match what the candidate specifies as usual). This makes it possible to prevent certain candidates from being used as default. A bean defined as:
@Bean @NotDefault @Red
ARedBean myRedBean() { ... }
Will only get injected into an injection point that specifies the @Red
annotation. Without the @NotDefault
annotation it would get injected into any injection point that matches the type.
If anyone would like to toy with this, define a configuration to use an alternative AutowireCandidateResolver
:
@Configuration
public static class Config {
@Bean
public CustomAutowireConfigurer autowireConfigurer(DefaultListableBeanFactory beanFactory) {
CustomAutowireConfigurer configurer = new CustomAutowireConfigurer();
beanFactory.setAutowireCandidateResolver(new NotDefaultSupportingQualifierAnnotationAutowireCandidateResolver());
configurer.postProcessBeanFactory(beanFactory);
return configurer;
}
}
And then use this implementation:
/**
* {@link AutowireCandidateResolver} implementation based on {@link ContextAnnotationAutowireCandidateResolver}
* that allows candidates to be annotated with a {@link NotDefault} annotation to indicate they should not
* be wired into injection points which have no annotations. If the injection points has at least one annotation
* which also matches the candidate, injection is allowed as normal.
*/
public class NotDefaultSupportingQualifierAnnotationAutowireCandidateResolver extends ContextAnnotationAutowireCandidateResolver{
private static final Annotation NOT_DEFAULT = AnnotationUtils.synthesizeAnnotation(NotDefault.class);
/**
* Match the given qualifier annotations against the candidate bean definition.
*/
@Override
protected boolean checkQualifiers(BeanDefinitionHolder bdHolder, Annotation[] annotationsToSearch) {
if (super.checkQualifiers(bdHolder, annotationsToSearch)) {
/*
* The qualifiers matched according to standard rules. If there were any custom annotations
* present on the injection point (aside from the Default annotation) then accept this result.
*/
if (annotationsToSearch != null) {
for (Annotation annotation : annotationsToSearch) {
if (annotation.annotationType() != Default.class && isQualifier(annotation.annotationType())) {
return true;
}
}
}
/*
* There were no custom annotations on the injection point, or there was only a Default annotation.
* This means the injection point expects a default candidate. Any candidate is a default candidate
* unless specifically annotated with NotDefault:
*/
return !checkQualifier(bdHolder, NOT_DEFAULT, new SimpleTypeConverter());
}
return false;
}
}
And these two annotations:
@Documented
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
public @interface NotDefault {
}
@Documented
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
public @interface Default {
}