spring-framework
spring-framework copied to clipboard
Introduce @Secondary beans in ressemblence of @Primary 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:
@Bean
public SomeType someTypeBean() {
return new SomeType();
}
and used as in:
@Bean
public SomeOtherType someOtherTypeBean(SomeType val) {
return new SomeOtherType(val);
}
I would like to being able to register a bean:
@Bean
@Secondary // mirrored behavior of @Primary
public SomeType someNewTypeBean() {
return new SomeType();
}
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.
I have a similar request, see https://github.com/spring-projects/spring-framework/issues/18201
FYI, I'm implementing this by introducing a customized @PriorityQualifier
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 @javax.annotation.Priority
to tweak the priority should do the trick.
Edit: Args, @Priority
cannot be declared on methods. So you need to use a component...
So maybe Spring might introduce its own @Priority
annotation in order to allow annotating beans?
Annotating someNewTypeBean with
@javax.annotation.Priority
to tweak the priority should do the trick.Edit: Args,
@Priority
cannot be declared on methods. So you need to use a component...So maybe Spring might introduce its own
@Priority
annotation in order to allow annotating beans?
My solution defer resolution of priority at injection point, so someNewTypeBean
maybe top priority at this injection and lowest priority at another injection.
In case if there tooo many default bean dependencies with autoconfigure, adding any
@Bean
public DataSource dataSource() {
....
}
will cause all the autoconfiguration fail where
@ConditionalOnMissingBean(DataSource.class)
So adding this annotation will allow such conditions to be treated as the bean is missing (and the existed one is secondary).
See also https://github.com/spring-projects/spring-boot/issues/9528 its about the same problem
@msangel I am not sure how this is related to this issue. Please review the Javadoc of @ConditionalOnMissingBean
that states this shouldn't be used on user configuration.
I want my @NonPrimary
annotation. Thanks.
If you are willing to customize Spring a bit, I wrote a ContextAnnotationAutowireCandidateResolver
that supports a NotDefault
annotation to indicate that a bean is "private" and will never be injected unless all qualifiers match.
See here: https://github.com/spring-projects/spring-framework/issues/26528#issuecomment-1149741516
The semantics might be slightly different than Secondary
(if I understand it correctly), but it might still be close to what you are looking for (or give you an idea on how to change the code). I use this for a similar purpose though.