0

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".

Matthieu Napoli
  • 48,448
  • 45
  • 173
  • 261

1 Answers1

0

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.
LaurentG
  • 11,128
  • 9
  • 51
  • 66
  • Wow OK thanks for the answer! I was expecting for a configuration option (something supported by Spring). And the example in my question is really an example, nothing formal). – Matthieu Napoli Jun 30 '13 at 17:27