简体   繁体   中英

Dynamically injecting a JDK dynamic proxy as spring bean - but only if no other implementation is available

I have a situation where I want to look for interfaces annotated with a given annotation, then check if a matching implementation is available. If not, I want to make a bean available that is actually a JDK proxy of the interface, essentially exactly what:

@ConditionalOnMissingBean 

would do, except without writing a factory method for each of those.

I have code that is working "sometimes" - it seems extremely sensitive to the classloader structure, specifically, wether classes are loaded from a springboot fat jar (works) or wether some part is loaded from a separate classpath entry (doesnt work, mostly).

here is what I am doing:

@Service
@Order(value = Ordered.LOWEST_PRECEDENCE)
public class RemotingImportService implements BeanFactoryPostProcessor {

    private static Log log = LogFactory.getLog(RemotingExportService.class);

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {


         // scan classpath with classgraph, then:
        for (ClassInfo classInfo : scanResult.getClassesWithAnnotation(MyAnnotation.class.getCanonicalName())) {

            Class c = Class.forName(classInfo.getName());

            if(beanFactory.getBeanNamesForType(c).length > 0) {
                implFound = true;
                log.info(c.getName()+" already has an implementation ... skipping");
                continue;
            }

            // create proxy, then:

            GenericBeanDefinition bdService = new GenericBeanDefinition();
            bdService.setBeanClassName(classInfo.getName());
            bdService.setInstanceSupplier(new ProxySupplier(proxy));
            bdService.setLazyInit(true);
            ((DefaultListableBeanFactory) beanFactory).registerBeanDefinition(classInfo.getName(), bdService);              
            log.info(c.getName()+" has NO implementation ... proxy registerd");

        }

in some cases, it seems that the beanfactory isn't finished, and

  beanFactory.getBeanNamesForType() 

returns an empty list, although it does find beans for that type later. i am aware that messing with this is probably not ideal - but it would be nice to find a solution that plays nice with spring boot.

any suggestions on how to solve this? a way to mark a bean definition as "ConditionalOnMissingBean" would also be great.

You should use BeanPostProcessor instead of BeanFactoryPostProcessor

BeanPostProcessor is operating by assembled beans, while BeanFactoryPostProcessor uses raw bean definitions. Read more here

public class ConditionalOnMissingProcessor implements BeanPostProcessor, Ordered, ApplicationContextAware
{
private static final Logger LOG = Logger.getLogger(ConditionalOnMissingProcessor .class);

private ApplicationContext applicationContext;

// Ordering to last in chain.
@Override
public int getOrder()
{
    return Ordered.LOWEST_PRECEDENCE;
}

@Override
public void setApplicationContext(final ApplicationContext applicationContext) throws BeansException
{
    this.applicationContext = applicationContext;
}

/**
 * Process each bean and inject proxy objects in fields marked with: {@ConditionalOnMissingBean}
 */
@Override
public Object postProcessBeforeInitialization(final Object bean, final String beanName) throws BeansException
{
    LOG.debug("Processing bean: " + beanName);
    final Class clazz = bean.getClass();
    ReflectionUtils.doWithFields(clazz, new ReflectionUtils.FieldCallback()
    {
        @Override
        public void doWith(final Field field)
        {
            try
            {
                if(field.isAnnotationPresent(ConditionalOnMissingBean.class))
                {
                    ReflectionUtils.makeAccessible(field);
                     final String beanFieldName = field.getName();
                    ...
                    // Find proper implementation in application context
                    // Or create a proxy.
                    // Inject proxy or impl into a bean
                    field.set(bean, <value>));

                }
            }
            catch(IllegalAccessException e)
            {
                LOG.error("Cannot set " + field.getName() + " in " + beanName, e);
            }
        }
    });

    return bean;
}

UPD:

First of all, I have to say about disadvantages of your impl (IMHO): - you are trying to scan classpath to find all interfaces with @RemoteEndpoint annotation, and, if current application context doesn't contain a bean that implemented this interface - create a proxy bean. But what if I say, that not all interfaces marked with @RemoteEndpoint should be taken into account? Developer should explicitly mark those interfaces, and then you can create all needed beans (for example, developer makes a common-services library). - probably you are specifying redudnant information, when marking impl class with @RemotingEndpoint(value=RandomService.class) annotation. You are already mentioned that when you implemented an interface.

There are multiple ways for implementing your idea

  1. Using custom annotation on bean fields instead of @Autowired .

    pros:

    • simply check all bean fields for presence of your custom annotation, and inject dependency (proxy or impl), using BeanPostProcessor .

    cons:

    • developer should mark all bean dependencies with custom annotation;
    • it won't work if developer would have to obtain new dependencies at runtime.
  2. Using regular @Autowired and @Value annotations for injecting remote service proxy In this case you should use BeanFactoryPostProcessor (as you already tried). You'll have to iterate over all bean definitions, collect a map of field names and interfaces of remote service proxies you'll have to register (dependencies meta info). And next step is creating and registering beans using dependencies meta info (creating new ones only if there is no implementation bean in context). Spring will autowire those fields later. But, those dependencies should be singleton beans.

    pros:

    • no mess with custom annotations;
    • works at runtime

    cons:

    • proxies are singletons (you need prototypes as diagram displayed).
  3. Still using regular @Autowired and @Value annotations and BeanFactoryPostProcessor , but instead of registering new beans, you should register a FactoryBean for each interface @RemoteEndpoint .

    pros:

    • no mess with custom annotations.
    • works at runtime
    • prototype-scoped proxies

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM