Search code examples
javaspringspring-annotationsspring-iocspring-config

Is there a way to make some beans created by @Bean distinguishable?


This is an exotic use-case, so it requires some patience to understand and may require exotic solutions.

The Context

I'm making a library to be used with Spring that performs automatic actions upon specific bean instances present in the context, created by @Bean methods and not by @ComponentScan. If at all possible, the beans should be distinguishable not by type, but by other means, preferably annotations on the factory method.

Here's the ideal case. E.g. let's say there are 2 bean-producing methods:

@Bean
public SomeType makeSome() {...}

@Bean
@Special
public SomeOtherType makeOther() {...}

Here, the second bean is special because of the @Special annotation on method that created it. But any mechanism of making it distinguishable is an option.

Then, I want to somehow get only the special beans.

The Caveat

I'm aware that if all the beans would implement the same interface, I could inject them by type. But this should work as transparently as possible, requiring as little change to an existing app as possible.

The potential approaches

Here's two broad approaches I have in mind:

1) Jack into the process of registering beans, and transparently wrap the bean instances into some sort of a container (I'm quite certain this part is doable). E.g.

public void registerBean(Object bean, ApplicationContext ctx) {
   ctx.register(bean); //do the usual
   ctx.register(new Wrapper(bean); //register it wrapped as well
}

Then, inject all all beans of type Wrapper. The problem here is obviously the duplication... Alternatively I could maybe generate a proxy instance on the fly that would implement a Wrapper interface, so it could at the same time act as the original bean and as a wrapper. I did say I'm OK with exotic solutions too, didn't I?

2) Spring already distinguishes bean candidates from actual registered beans (e.g. @ComponentScan can filter the candidates by package, annotations etc). I'm hoping to maybe jack into this process and get a hold of candidate descriptors that still contain some useful metadata (like their factory method) that would allow me to later distinguish those bean instances.


Solution

  • Seems like you need to use @Qualifier it provides functionality to distinguish beans:

    @Bean
    @Qualifier("special")
    class MyBean {}
    
    @Bean
    class OtherBean {
        @Qualifier("special")
        private MyBean bean;
    }
    

    You can read about it more there: https://spring.io/blog/2014/11/04/a-quality-qualifier

    UPD (understood what you are talking about :) ) You may want to take a look at BeanDefinitionRegistryPostProcessor

    Here a usage sample:

    import org.springframework.beans.BeansException;
    import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition;
    import org.springframework.beans.factory.config.BeanPostProcessor;
    import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
    import org.springframework.beans.factory.support.BeanDefinitionRegistry;
    import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
    
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.concurrent.ConcurrentMap;
    
    import static java.util.Collections.unmodifiableMap;
    
    /**
     * This is hack to collect all beans of some type in single map without eager initialization of those beans
     *
     * Usage:
     * 1. Register new bean of type {@link ServiceTrackingBeanPostProcessor} parametrized with class of
     *    beans you want to collect
     * 2. Now you can inject {@link ServiceTracker} parametrized with your type anywhere
     *
     * @param <T> Located type
     */
    public class ServiceTrackingBeanPostProcessor<T> implements BeanPostProcessor, BeanDefinitionRegistryPostProcessor {
        private final ConcurrentMap<String, T> registeredBeans = new ConcurrentHashMap<>();
        private final Class<T> clazz;
        private final String beanName;
    
        public ServiceTrackingBeanPostProcessor(Class<T> clazz) {
            this.clazz = clazz;
            beanName = "locatorFor" + clazz.getCanonicalName().replace('.', '_');
        }
    
        @Override
        public Object postProcessBeforeInitialization(Object o, String s) throws BeansException {
            return o;
        }
    
        @Override
        public Object postProcessAfterInitialization(Object o, String s) throws BeansException {
            if (!clazz.isInstance(o)) {
                return o;
            }
            registeredBeans.putIfAbsent(s, (T) o);
            return o;
        }
    
        @Override
        public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
            AnnotatedGenericBeanDefinition def = new AnnotatedGenericBeanDefinition(Wrapper.class);
            registry.registerBeanDefinition(beanName, def);
        }
    
        @Override
        public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
            beanFactory.registerSingleton(beanName, new Wrapper(unmodifiableMap(registeredBeans)));
        }
    
        private class Wrapper extends AbstractServiceTracker<T> {
            public Wrapper(Map<String, T> services) {
                super(services);
            }
        }
    }
    

    You just need to change the condition of check from clazz.isInstance to obtainment of beanDefinition from application context by bean name where you could get almost any information about instantiation and annotations