spring-framework icon indicating copy to clipboard operation
spring-framework copied to clipboard

Introduce @Secondary beans in ressemblence of @Primary beans.

Open raphw opened this issue 2 years ago • 8 comments

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.

raphw avatar Dec 08 '20 19:12 raphw

I have a similar request, see https://github.com/spring-projects/spring-framework/issues/18201

quaff avatar Dec 09 '20 10:12 quaff

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;

	}

}

quaff avatar Dec 09 '20 10:12 quaff

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?

OLibutzki avatar Dec 09 '20 12:12 OLibutzki

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.

quaff avatar Dec 09 '20 13:12 quaff

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 avatar Apr 09 '21 05:04 msangel

@msangel I am not sure how this is related to this issue. Please review the Javadoc of @ConditionalOnMissingBeanthat states this shouldn't be used on user configuration.

snicoll avatar Apr 09 '21 13:04 snicoll

I want my @NonPrimary annotation. Thanks.

onacit avatar Nov 10 '21 09:11 onacit

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.

hjohn avatar Aug 10 '22 17:08 hjohn