1

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.

kaqqao
  • 12,984
  • 10
  • 64
  • 118
  • Have you tried `@Qualifier`? – chrylis -cautiouslyoptimistic- Feb 05 '18 at 16:29
  • @chrylis Qualifier will make a single bean distinguishable, but I need a wat to make a whole group of beans distinguishable and injectable together, , without knowing the ID of each. So it's not applicable. – kaqqao Feb 05 '18 at 17:09
  • 1
    @kaqqao You can apply a qualifier to multiple beans and then inject a qualified collection. – chrylis -cautiouslyoptimistic- Feb 05 '18 at 17:27
  • What about a component scan by yourself and deep scan the factory methods for additional annotations. Then you enrich the bean candidates with the found annotation or wrap them with proper proxies? – Martin Frey Feb 05 '18 at 22:14
  • 1
    @chrylis If combined with the fact that `Qualifier` can be used as a meta-annotation, this is actually exactly the correct approach! Thanks for leading me to it. – kaqqao Feb 05 '18 at 22:45
  • I accepted to mark my question as a duplicate, and I'll post my final findings on the original question. – kaqqao Feb 05 '18 at 23:00

3 Answers3

2

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

smt
  • 91
  • 1
  • 9
  • This is exactly along my original line of thinking. And it would likely work (so I upvoted it). But I fortunately found a simpler solution that I'll post to the question this one is marked as duplicate of. – kaqqao Feb 05 '18 at 23:01
  • I was originally going to delete the question, but this answer was so useful for so many purposes that it shouldn't be lost. I'll accept it as the answer because it certainly could solve my issue. Thanks again! – kaqqao Feb 06 '18 at 13:18
1

Here is one way to get all beans with @Special annotation.

Map<String,Object> beans = applicationContext.getBeansWithAnnotation(Special.class);

Reference: https://stackoverflow.com/a/14236573/1490322

EDIT: The above answer seems to work only when the class is annotated with @Special, so it will not work for your scenario. But this other answer to the same question might work. It uses meta data from the ConfigurableListableBeanFactory to identify any beans whose methods were annotated with a specific annotation.

Jose Martinez
  • 11,452
  • 7
  • 53
  • 68
  • Doesn't this expect the bean _classes_ to be annotated with `@Special`? It's important not to have modify the classes, but only the factory methods. I'll test it. – kaqqao Feb 05 '18 at 17:05
  • I think you are right. I wonder why that answer was upvoted so many times. That question does have an accepted answer that may work. – Jose Martinez Feb 05 '18 at 18:39
0

I think as mentioned @Qualifier annotation is a way to go here. But I will show different example:

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

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

in your component (@Service) where you want this bean you can:

@Autowired
@Qualifier("makeSome")
SomeType makeSomeBean;

As you see two beans with same type can be distinguished by bean name (Bean assigned with same name as @Bean annotated method named)

P_M
  • 2,723
  • 4
  • 29
  • 62