Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamically defining which bean to autowire in Spring (using qualifiers)

Tags:

java

spring

I have a Java EE + Spring app that favors annotations over XML configuration. The beans always have prototype scope.

I now have in my app business rules that depend on the country the user request was made from. So I would have something like this (keep in mind this example was heavily simplified):

@Component
public class TransactionService {
    @Autowired
    private TransactionRules rules;
    //..
}


@Component
@Qualifier("US")
public class TransactionRulesForUS implements TransactionRules {
     //..
}

@Component
@Qualifier("CANADA")
public class TransactionRulesForCanada implements TransactionRules {
     //..
}

I was looking for a way to make the auto-wiring mechanism automatically inject the right bean (either US or Canada, in this example) based on the country of the current request. The country would be stored in a ThreadLocal variable, and it would change in each request. There would also be a global class, for all countries that didn't have their own particular rules.

I imagine I would have to customize the way Spring decides how to create the objects it will inject. The only way I found to do this was using FactoryBean, but that was not quite what I hoped for (not generic enough). I was hoping to do something like this:

  1. Before Spring instantiates an object, my own custom code would have to be called.
  2. If I detect that the interface being requested has more than one implementation, I would look up in my ThreadLocal variable the right country and would dynamically add the appropriate Qualifier to the auto-wire request.
  3. After that, Spring would do all its usual magic. If a qualifier was added, that would have to be taken in consideration; if it hasn't, the flow would proceed as usual.

Am I in the right path? Any ideas for me on this?

Thanks.

like image 455
zleand Avatar asked Oct 11 '11 01:10

zleand


People also ask

Can we use @qualifier and @bean together?

NOTE: if you are creating bean with @Bean, it will be injected byType if there is duplicates then it will injected byName. we no need to mention @Bean(name="bmwDriver") . so you can directly use qualifier("bmwDriver") wherever you need in classes.

Can we use qualifier with Autowired?

You can use @Qualifier along with @Autowired to help Spring Framework find the right bean to autowire. Spring Framework will ask you explicitly select the bean if ambiguous bean types are found, in which case you should provide the qualifier.

How do you make Spring beans dynamically?

It is possible to register beans dynamically by using BeanFactoryPostProcesor . Here you can do that while the application is booting (spring's application context has been initialized).


1 Answers

Create your own annotation that is used to decorate instance variables or setter methods, then a post-processor that processes the annotation and injects a generic proxy which resolves the correct implementation at runtime and delegates the call to it.

@Component
public class TransactionService {
  @LocalizedResource
  private TransactionRules rules;
  //..
}

@Retention(RUNTIME)
@Target({FIELD, METHOD})
public @interface LocalizedResource {}

Here is the algorithm for the postProcessBeforeInitialization(bean, beanName) method in your bean post-processor :

  1. Introspect the bean class in order to find instance variables or setter methods which are annotated with @LocalizedResource. Store the result in a cache (just a map) indexed by the class name. You can use Spring's InjectionMetadata for this purpose. You can look for examples on how it works by searching references to this classe in spring code.
  2. If such a field or method exists for the bean, create a proxy using the InvocationHandler described below, passing it the current BeanFactory (the bean post-processor has to be ApplicationContextAware). Inject that proxy in the instance variable, or invoke the setter method with the proxy instance.

Here is the InvocationHandler for the proxy that will be used to create localized resources.

public class LocalizedResourceResolver implements InvocationHandler {
  private final BeanFactory bf;
  public LocalizedResourceResolver(BeanFactory bf) {
    this.bf = bf;
  }
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    String locale = lookupCurrentLocale();
    Object target = lookupTarget(locale);
    return method.invoke(target, args);
  }

  private String lookupCurrentLocale() {
    // here comes your stuff to look up the current locale
    // probably set in a thread-local variable
  }

  private Object lookupTarget(String locale) {
    // use the locale to match a qualifier attached to a bean that you lookup using the BeanFactory.
    // That bean is the target
  }
}

You may need to make some more controls over the bean type, or add the requested bean type in the InvocationHandler.

The next thing is to autodetect implementations of a given interface, which are local-dependant, and register them with the qualifier corresponding to the locale. You can implement a BeanDefinitionRegistryPostProcessor or BeanFactoryPostProcessor for that purpose, in order to add new BeanDefinitions to the registry, with proper qualifier, one for each implementation of locale-aware interfaces. You can guess the locale of an implementation by following naming conventions : if a locale-aware interface is called TransactionRules, then implementations may be named TransactionRules_ISOCODE in the same package.

If you cannot afford such a naming convention, you will need to have some sort of classpath scanning + a way to guess the locale of a given implementation (maybe an annotation on the implementation classes). Classpath scanning is possible but quite complex and slow, so try to avoid it.

Here's a summary of what happens:

  1. When the application starts up, implementations of TransactionRules will be discovered and bean definitions will be created for each of them, with a qualifier corresponding to the locale of each implementation. The bean name for these beans is not relevant as lookup is performed based on type and qualifier.
  2. During execution, set the current locale in a thread-local variable
  3. Lookup the bean you need (eg. TransactionService). The post-processor will inject a proxy for each @LocalizedResource instance fields or setter method.
  4. When invoking a method on TransactionService that ends up into some TransactionRules' method, the invocation handler bound to the proxy switches to the correct implementation based on the value stored in the thread-local variable, then delegates the call to that implementation.

Not really trivial, but it works. This is actually how @PersistenceContext is processed by Spring, except for implementations lookup, which is an additional feature of your use case.

like image 173
Gaetan Avatar answered Sep 28 '22 17:09

Gaetan