This blog discuss a DI implementation pattern on
Play model class. However it might also be helpful for developer using other MVC frameworks.
As stated in
this page, Model should NOT be
kept as a set of simple Java Beans and leave the application logic back into a “service” layer which operates on the model objects
.
So there are often good reasons to call to some services utilities/executing tasks from within your model class in order to complete an application logic inside the model class. Here is one example in my recent development work. When users login to the web site, they needs to be able to turn on parent mode for some time, say 1 hour. The parent mode setting will automatically turn off as timeout.
The first version of turnOnParentMode() is like
public void turnOnParentMode(int timeout) {
if (parentMode) return;
if (null == ip) throw new IllegalStateException("Cannot set parent mode when IP address is null");
parentMode = true;
TimeKeeper.register(username, timeout);
}
As you can see, TimeKeeper::register is implemented as a static class member function so that we can easily call it from anywhere, like from within a Model class. There is nothing wrong with that approach, unless later I decide to unit test this method with JMock. The immediate question I need to answer is how can I mock TimeKeeper::register as it is a static class member?
Vampires (if you don't know who are vampires, take a look at
this blog) from the underworld told me that
DJ is the answer to questions like the above one. In the beginning they advocate a beast named
Spring, but recently they look like be more addicted to a monster called
Guice. Since I decide to be part of them I turned to Guice to look for my answer.
Everything is good as I quickly found Guice is supported by Play with
play-guice plugin. Yes so far so good until I read this statement: "... inject into your application’s Controller, Job and Mail classes (based on @javax.inject.Inject)". What? injection can only be operate on Controller, Job and Mail classes? Why my poor model class is not in the list?
Alright, let's do something to get it work! First I will change the implementation of turnOnParentMode() to:
@javax.inject.Inject private static timer;
public void turnOnParentMode(int timeout) {
if (parentMode) return;
if (null == ip) throw new IllegalStateException("Cannot set parent mode when IP address is null");
parentMode = true;
timer.register(username, timeout);
}
And of couse, TimeKeeper::register method will become a non-static instance member. Now we are ready for injection.
Since GuicePlugin won't inject Model class, specifically Playframework won't inject Model class, we need to hack the code of GuicePlugin by adding a new method:
private void injectModelClasses(BeanSource source) {
List classes = Play.classloader.getAssignableClasses(Model.class);
for(Class clazz : classes) {
for(Field field : clazz.getDeclaredFields()) {
if(Modifier.isStatic(field.getModifiers()) && field.isAnnotationPresent(Inject.class)) {
Class type = field.getType();
field.setAccessible(true);
try {
field.set(null, source.getBeanOfType(type));
} catch(RuntimeException e) {
throw e;
} catch(Exception e) {
throw new RuntimeException(e);
}
}
}
}
}
And add the following line to the end of onApplicationStart() method:
injectModelClasses(this);
Okay, let's compile GuicePlugin by running ant, and then "play run", everything is okay, TimeKeeper debug info shows out, looks like our code is good, and more importantly we don't rely on static TimeKeeper::register method, we have some flexibility to inject different implementation of TimeKeeper instance to our model class.
But wait, we haven't solve the problem of unit test turnOnParentMode() method yet, how to replace the injection with a mock object of TimeKeeper? I will answer this question in next blog.
BTW, a suggestion to Play team: Add a new interface say: InjectSupport, and have ControllerSupport, Mail and Job class to extend/implement the InjectSupport tag interface, and change the code of Injector::inject to:
public static void inject(BeanSource source) {
//List classes = Play.classloader.getAssignableClasses(ControllerSupport.class);
//classes.addAll(Play.classloader.getAssignableClasses(Mailer.class));
//classes.addAll(Play.classloader.getAssignableClasses(Job.class));
List classes = Play.classloader.getAssignableClasses(InjectSupport.class)
for(Class clazz : classes) {
for(Field field : clazz.getDeclaredFields()) {
if(Modifier.isStatic(field.getModifiers()) && field.isAnnotationPresent(Inject.class)) {
Class type = field.getType();
field.setAccessible(true);
try {
field.set(null, source.getBeanOfType(type));
} catch(RuntimeException e) {
throw e;
} catch(Exception e) {
throw new RuntimeException(e);
}
}
}
}
}
This way we are not limited DI to only Controller, Mail and Job classes, we add the flexibility to enable developer to inject any class he/she want using play-guice plugin.