@Bean: unique bean name in subclasses

I have an abstract configuration class in a Spring 4 based project. The class looks something like this:

public abstract class C<B> {
    @Bean
    public B b() {
        return a();
    }

    protected abstract B a();
}

Of course, this is not the actual class, but let's work with this theoretical class, that involves the most important parts of the real problematic class.

This class C is extended by multiple classes. For example:

@Configuration
public class E extends C<X> {

    @Override
    protected X a() { return new X(); }
}

@Configuration
public class F extends C<Y> {

    @Override
    protected Y a() { return new Y(); }
}

The goal is to let every extending class instantiate and set up the bean the way they are intended to be and to expose the instance as a Spring bean to be injected into other beans. In other words, all of the fields should have a bean instance injected in the following bean:

@Component
public class Bean {
    private final X x;
    private final Y y;

    public Bean(X x, Y y) {
        this.x = x;
        this.y = y;
    }
}

The problem is that the bean created by b() will always have a name of b, which means that even if I have more than 1 extension of C, only one of the beans will be available. In the above example, it would mean that either x, or y field is null (or would throw an exception).

How can I give unique names to these beans, so that all of the extensions of C can expose its own bean instance without overlap?

Upvotes: 5

Views: 1110

Answers (2)

sirjoga
sirjoga

Reputation: 111

Seems there is no way to override naming strategy of @Bean beans.

But you can create your custom annotation, for example @UniqueNamedBean, and register your custom BeanFactoryPostProcessor that scans methods of existing bean definition classes and creates additional bean definitions.

This is kotlin, not pure java, but hope it helps.

@Component
class UniqueNamedBeanPostProcessor : BeanFactoryPostProcessor {
    override fun postProcessBeanFactory(beanFactory: ConfigurableListableBeanFactory) {
        for (name in beanFactory.beanDefinitionNames) {
            val def = beanFactory.getBeanDefinition(name) as? AnnotatedBeanDefinition ?: continue
            val className = def.beanClassName
            if (className == null || def.factoryMethodName != null) continue
            val metadata = def.metadata
            for (method in metadata.getAnnotatedMethods(UniqueNamedBean::class.java.name)) {
                val beanDef = GenericBeanDefinition()
                beanDef.factoryBeanName = name
                beanDef.factoryMethodName = method.methodName
                method.getAnnotationAttributes(Scope::class.qualifiedName!!)?.let {
                    beanDef.scope = it["value"]!! as String
                }
                beanDef.autowireMode = AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR
                (beanFactory as DefaultListableBeanFactory).registerBeanDefinition(
                    className + "_" + method.methodName, beanDef
                )
            }
        }
    }
}

Upvotes: 1

J11
J11

Reputation: 434

Indeed, without specifying the bean name it will fail to start with the following message:

The bean 'b', defined in class path resource

Here's a way, which is counter intuitive because if the subclass extender doesn't call super.b(), then some important logic would never be executed.

But for the sake of answering your specific question:

C superclass

public abstract class C<B> {

  protected B b() {
   return a();
  }

  protected abstract B a();
}

Extending subclasses

@Configuration
public class E extends C<X> {

  @Bean("X)// Bean name qualifier
  @Override
  protected X b() {
    return super.b();
  }

  @Override
  protected X a() {
    return new X();
  }
}


@Configuration
public class F extends C<Y> {

  @Bean("Y") // Bean name qualifier
  @Override
  protected Y b() {
    return super.b();
  }

  @Override
  protected Y a() {
    return new Y();
  }
}

Spring component

@Component
public class Bean {
  private final X x;
  private final Y y;

  public Bean(X x, Y y) {
    this.x = x;
    this.y = y;
  }
}

Hope this helps

Upvotes: 1

Related Questions