Create your own annotation, which is used to decorate instance variables or setter methods, and then a postprocessor that processes the annotation and enters a common proxy server that allows the correct implementation at run time and delegates the call.
@Component public class TransactionService { @LocalizedResource private TransactionRules rules; //.. } @Retention(RUNTIME) @Target({FIELD, METHOD}) public @interface LocalizedResource {}
Here is the algorithm of the postProcessBeforeInitialization(bean, beanName) method in the bean post processor:
- Imagine a bean class to find instance variables or customization methods that are annotated with @LocalizedResource. Store the result in a cache (map only) indexed by class name. You can use Spring
InjectionMetadata for this purpose. You can find examples of how this works by looking for references to this class in Spring code. - If such a field or method exists for the bean, create a proxy server using the InvocationHandler described below, passing it the current BeanFactory (the bean post processor must be ApplicationContextAware). Add this proxy to the instance variable or call the setter method with the proxy instance.
Here is the InvocationHandler for the proxy server 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 a few more controls over the bean type or add the requested bean type to the InvocationHandler.
The next is to automatically detect implementations of this interface that are locally dependent, and register them using a classifier that matches the language. For this purpose, you can implement BeanDefinitionRegistryPostProcessor or BeanFactoryPostProcessor to add a new BeanDefinition to the BeanDefinition with the appropriate qualifier, one for each implementation of interfaces that support the locale. You can guess the implementation language by following naming conventions: if the language-oriented interface is called TransactionRules, then implementations can be called TransactionRules_ISOCODE in the same package.
If you cannot afford such a naming convention, you will need to have some kind of class scan / way of guessing the locale of a given implementation (possibly annotation on implementation classes). Scanning by class is possible, but rather difficult and slow, so try to avoid this.
Here is a brief description of what is happening:
- When the application starts, TransactionRules implementations will be detected and bean definitions will be created for each of them with a qualifier corresponding to the language of each implementation. The bean name for these beans does not matter, because the search is based on the type and classifier.
- At run time, set the current locale to a local local variable
- Find the desired bean (e.g. TransactionService). The post processor will introduce a proxy for each field of the @LocalizedResource instance or installation method.
- When you call the TransactionService method, which falls into some TransactionRules method, the call handler associated with the proxy switches to the correct implementation based on the value stored in the local variable of the stream, and then delegates the call to this implementation.
Not entirely trivial, but it works. This is in fact how @PersistenceContext is handled by Spring, with the exception of finding implementations, which is an additional feature of your use case.