-
Notifications
You must be signed in to change notification settings - Fork 38.2k
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
Introduce support for non-@Primary fallback beans #26241
Comments
I have a similar request, see #18201 |
FYI, I'm implementing this by introducing a customized import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ ElementType.FIELD, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface PriorityQualifier {
String[] value() default {};
} import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;
import org.springframework.core.ResolvableType;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
@Component
@Slf4j
public class PriorityQualifierPostProcessor implements BeanPostProcessor, PriorityOrdered, BeanFactoryAware {
private BeanFactory beanFactory;
private Map<String, Boolean> logged = new ConcurrentHashMap<>();
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 1;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
ReflectionUtils.doWithFields(bean.getClass(), field -> {
inject(bean, beanName, field);
}, this::filter);
ReflectionUtils.doWithMethods(bean.getClass(), method -> {
inject(bean, beanName, method);
}, this::filter);
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
private void inject(Object bean, String beanName, Method method) {
ReflectionUtils.makeAccessible(method);
doInject(bean, beanName, method, () -> {
String methodName = method.getName();
if (methodName.startsWith("set") && methodName.length() > 3)
methodName = StringUtils.uncapitalize(methodName.substring(3));
return methodName;
}, () -> ResolvableType.forMethodParameter(method, 0), (b, candidate) -> {
try {
method.invoke(b, candidate);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
});
}
private void inject(Object bean, String beanName, Field field) {
ReflectionUtils.makeAccessible(field);
doInject(bean, beanName, field, field::getName, () -> ResolvableType.forField(field), (b, candidate) -> {
try {
field.set(b, candidate);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
});
}
private void doInject(Object bean, String beanName, AccessibleObject methodOrField,
Supplier<String> defaultCandidate, Supplier<ResolvableType> typeSupplier,
BiConsumer<Object, Object> injectConsumer) {
String injectPoint = defaultCandidate.get();
PriorityQualifier pq = methodOrField.getAnnotation(PriorityQualifier.class);
String[] candidates = pq.value();
if (candidates.length == 0)
candidates = new String[] { injectPoint };
for (String name : candidates) {
if (beanFactory.containsBean(name)) {
ResolvableType rt = typeSupplier.get();
boolean typeMatched = beanFactory.isTypeMatch(name, rt);
if (!typeMatched) {
Class<?> rawClass = rt.getRawClass();
typeMatched = (rawClass != null) && beanFactory.isTypeMatch(name, rawClass);
}
if (typeMatched) {
injectConsumer.accept(bean, beanFactory.getBean(name));
if (logged.putIfAbsent(beanName + "." + injectPoint, true) == null) {
// remove duplicated log for prototype bean
log.info("Injected @PrioritizedQualifier(\"{}\") for field[{}] of bean[{}]", name, injectPoint,
beanName);
}
break;
} else {
log.warn("Ignored @PrioritizedQualifier(\"{}\") for {} because it is not type of {}, ", name,
beanName, rt);
}
}
}
}
private boolean filter(AccessibleObject methodOrField) {
return methodOrField.isAnnotationPresent(Autowired.class)
&& methodOrField.isAnnotationPresent(PriorityQualifier.class);
}
} import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = PriorityQualifierTest.TestConfiguration.class)
public class PriorityQualifierTest {
@Autowired
@PriorityQualifier("prioritizedTestBean")
private TestBean testBean;
@Autowired
@PriorityQualifier
private TestBean prioritizedTestBean;
private TestBean testBean2;
private TestBean testBean3;
@Autowired
@PriorityQualifier("prioritizedTestBean")
void setTestBean(TestBean testBean) {
this.testBean2 = testBean;
}
@Autowired
@PriorityQualifier
void setPrioritizedTestBean(TestBean prioritizedTestBean) {
this.testBean3 = prioritizedTestBean;
}
@Test
public void testExplicitFieldInjection() {
assertThat(testBean.getName(), is("prioritizedTestBean"));
}
@Test
public void testImplicitFieldInjection() {
assertThat(prioritizedTestBean.getName(), is("prioritizedTestBean"));
}
@Test
public void testExplicitSetterInjection() {
assertThat(testBean2.getName(), is("prioritizedTestBean"));
}
@Test
public void testImplicitSetterInjection() {
assertThat(testBean3.getName(), is("prioritizedTestBean"));
}
@Configuration
static class TestConfiguration {
@Bean
static PriorityQualifierPostProcessor priorityQualifierPostProcessor() {
return new PriorityQualifierPostProcessor();
}
@Bean
@Primary
public TestBean testBean() {
return new TestBean("testBean");
}
@Bean
public TestBean prioritizedTestBean() {
return new TestBean("prioritizedTestBean");
}
}
@RequiredArgsConstructor
static class TestBean {
@Getter
private final String name;
}
} |
Annotating someNewTypeBean with Edit: Args, So maybe Spring might introduce its own |
My solution defer resolution of priority at injection point, so |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
I want my |
If you are willing to customize Spring a bit, I wrote a See here: #26528 (comment) The semantics might be slightly different than |
almost 3 years later and no updates here ? |
@Secondary
fallback beans
Sadly, it seems that's not the case. If you set |
related: #31544 (discussion about a simple and more versatile solution for a particular subcase) |
Or see this potential solution, inspired by CDI: #26528 (comment) |
@FyiurAmron if you are interested in testing this in a real Spring application, the code you need to do so is: package notdefault;
import org.springframework.beans.SimpleTypeConverter;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.beans.factory.support.AutowireCandidateResolver;
import org.springframework.context.annotation.ContextAnnotationAutowireCandidateResolver;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
/**
* {@link AutowireCandidateResolver} implementation based on {@link ContextAnnotationAutowireCandidateResolver}
* that allows candidates to be annotated with a {@link NotDefault} annotation to indicate they should never
* be wired into injection points which have no annotations. If the injection point has at least one qualifier
* annotation which matches with a qualifier on such a not default candidate, injection is allowed as normal.
*/
class NotDefaultSupportingQualifierAnnotationAutowireCandidateResolver extends ContextAnnotationAutowireCandidateResolver {
private static final Annotation NOT_DEFAULT = AnnotationUtils.synthesizeAnnotation(NotDefault.class);
@Override
protected boolean checkQualifiers(BeanDefinitionHolder bdHolder, Annotation[] annotationsToSearch) {
if (!super.checkQualifiers(bdHolder, annotationsToSearch)) {
return false;
}
/*
* The qualifiers (if any) on the injection point matched the candidate's qualifiers according
* to the standard rules (a candidate always must have at least all qualifiers specified by the
* injection point).
*/
if (annotationsToSearch != null) {
/*
* If there was at least one qualifier on the injection point, or it has the Any annotation,
* then proceed with injection (note: we only need to find if a qualifier was *present* here, as
* all were already matched by checkQualifiers at the start of this method).
*/
for (Annotation annotation : annotationsToSearch) {
Class<? extends Annotation> annotationType = annotation.annotationType();
if (annotationType == Any.class || isQualifier(annotationType)) {
return true;
}
}
}
/*
* There were no qualifiers on the injection point at all. 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());
}
@Override
public boolean isAutowireCandidate(BeanDefinitionHolder bdHolder, DependencyDescriptor descriptor) {
/*
* Note: this method does not call super, but integrates all the super code into this method.
* This is because the code in QualifierAnnotationAutowireCandidateResolver is calling
* checkQualifiers twice (once with annotations on the field/parameter, and another time
* with the annotations on the method/constructor (if applicable)) and this causes the
* second check to often fail for NotDefault beans (as there are often no annotations). Instead,
* for proper NotDefault support, this must be a single check with all annotations concatenated.
*/
if (!bdHolder.getBeanDefinition().isAutowireCandidate()) {
return false;
}
if (!checkGenericTypeMatch(bdHolder, descriptor)) {
return false;
}
Annotation[] annotations = descriptor.getAnnotations();
MethodParameter methodParam = descriptor.getMethodParameter();
if (methodParam != null) {
Method method = methodParam.getMethod();
if (method == null || void.class == method.getReturnType()) {
Annotation[] methodAnnotations = methodParam.getMethodAnnotations();
if (methodAnnotations.length != 0) {
int originalLength = annotations.length;
annotations = Arrays.copyOf(annotations, originalLength + methodAnnotations.length);
System.arraycopy(methodAnnotations, 0, annotations, originalLength, methodAnnotations.length);
}
}
}
return checkQualifiers(bdHolder, annotations);
}
} And to use it: package notdefault;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.support.GenericApplicationContext;
/**
* Installs the {@link NotDefaultSupportingQualifierAnnotationAutowireCandidateResolver}
* via a Spring {@link ApplicationContextInitializer}.
*/
public class NotDefaultBeansInitializer implements ApplicationContextInitializer<GenericApplicationContext> {
@Override
public void initialize(GenericApplicationContext applicationContext) {
applicationContext
.getDefaultListableBeanFactory()
.setAutowireCandidateResolver(new NotDefaultSupportingQualifierAnnotationAutowireCandidateResolver());
}
} And the annotation classes: package notdefault;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Bean definition annotation which indicates this bean should not be injected into injection points
* which do not have at least one matching qualifier. Beans defined in this way will therefore not
* serve as default beans for injection points even if their types match.
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
public @interface NotDefault {
} package notdefault;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Injection point annotation to indicate that even not default candidates (annotated with {@link NotDefault})
* are suitable for injection. Any other qualifiers on the injection point must still match.
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
public @interface Any {
} |
@hjohn I went with a custom override to the
This gets injected via BTW I'm open for comments, this is the WIP implementation (although it does work nicely so far in both tests and organic code). |
Along with #26528, I'm considering a |
@Secondary
fallback beans@Primary
fallback beans
@Primary
fallback beans
Sometimes, one wants to be able to register a bean of a given type without breaking existing code, especially in multi-module projects. Assuming that a bean is already available:
and used as in:
I would like to being able to register a bean:
without disturbing existing code. If the someTypeBean is missing, it should fallback to someNewTypeBean, this would also allow for much smoother migrations in the case of multiple profiles.
The text was updated successfully, but these errors were encountered: