Search code examples
javaspringspring-ioc

Inject based on the package of the class


I have 2 modules containing classes:

blog.model.ArticleDAO
blog.model.CategoryDAO

users.model.UserDAO
users.model.UserGroupDAO

All these DAOs have a dependency on the same service, but I need to inject a different instance based on the package.

I mean the module blog should have a specific instance of MyService, and the module users should have another instance of MyService.

I don't want to create 2 named services because some day I may want to use the same service for all DAOs. Or I could also want to inject another specific instance for a specific class...

Is there a way to inject a service based on the package of a class?

A way to say:

  • inject foo (instance of MyService) into classes that are in blog.*
  • inject bar (instance of MyService) into classes that are in users.*

but keeping all my classes unaware of that! Their configuration should only state "Inject an instance of MyService".


Solution

  • First I want to say, I find this a strange requirement. I am also wondering why your DAOs need a Service. In a normal layered design, this is the opposite (the Service uses the DAO).

    However I find the challenge interesting, I tried to use a FactoryBean to create a Java Proxy class which would redirect at runtime to the correct instance of MyService depending of the caller package. Here is the code:

    public class CallerPackageAwareProxyFactoryBean implements
            FactoryBean<MyService>, ApplicationContextAware {
    
        private Class<?> targetServiceType;
        private ApplicationContext applicationContext;
    
        private InvocationHandler invocationHandler = new InvocationHandler() {
            public Object invoke(Object proxy, Method method, Object[] args)
                    throws Throwable {
                if (ReflectionUtils.isEqualsMethod(method)) {
                    // Only consider equal when proxies are identical.
                    return (proxy == args[0]);
                } else if (ReflectionUtils.isHashCodeMethod(method)) {
                    // Use hashCode of service locator proxy.
                    return System.identityHashCode(proxy);
                } else if (ReflectionUtils.isToStringMethod(method)) {
                    return "Service dispatcher: " + targetServiceType.getName();
                } else {
                    String callerPackageFirstLevel = getCallerPackageFirstLevel();
                    Map<String, ?> beans = applicationContext
                            .getBeansOfType(targetServiceType);
                    for (Map.Entry<String, ?> beanEntry : beans.entrySet()) {
                        if (beanEntry.getKey().startsWith(callerPackageFirstLevel)) {
                            return method.invoke(beanEntry.getValue(), args);
                        }
                    }
                    throw new IllegalArgumentException(
                            String.format(
                                    "Could not find any valid bean to forward call for method %s.",
                                    method.getName()));
                }
            }
    
            private String getCallerPackageFirstLevel() {
                Throwable t = new Throwable();
                StackTraceElement[] elements = t.getStackTrace();
    
                String callerClassName = elements[3].getClassName();
                return callerClassName.split("\\.")[0];
            }
        };
    
        public MyService getObject() throws Exception {
            return (MyService) Proxy.newProxyInstance(Thread.currentThread()
                    .getContextClassLoader(), new Class<?>[] { MyService.class },
                    invocationHandler);
        }
    
        public Class<?> getObjectType() {
            return MyService.class;
        }
    
        public boolean isSingleton() {
            return true;
        }
    
        public void setApplicationContext(ApplicationContext applicationContext) {
            this.applicationContext = applicationContext;
        }
    
        public void setTargetServiceType(Class<?> targetServiceType) {
            this.targetServiceType = targetServiceType;
        }
    
    }
    

    I didn't had to change anything to the Dao or Service configuration. I just had to add the creation of the FactoryBean in the Spring context:

    <bean id="myService" class="stackoverflow.CallerPackageAwareProxyFactoryBean">
        <property name="targetServiceType" value="a.b.c.MyService" />
    </bean>
    

    Maybe a few comments:

    • The caller package can only be get by creating an exception and looking at the stacktrace.
    • The code of the InvocationHandler is inspired from ServiceLocatorFactoryBean.
    • I am still wondering if there is an easier way but I think there is not.
    • You could replace part of the InvocationHandler to use a configuration Map (package => MyService bean name)
    • I would not recommend using such code in a productive environment.