|
GuiceZeroTurnaroundConversion
An example conversion of Guice to enable Zero Turnaround development on top of JavaRebel SDK.
IntroductionThis is a do-by-example guide for converting an existing project to enable zero turnaround in development. Although every project is different there are some general approaches one can take. Guice is a dependency injection framework based almost solely on Java annotations. It serves a perfect example of conversion to ZT for the following reasons:
Step 1: Resetting the reflection cacheAlmost all projects cache the results of reflection to save on lookup time. This usually includes methods, fields and constructors. Usually it will be easy enough to reset the cache when the class changes. In Guice case the reflection results are cached as precreated Injectors in the injectors field in InjectorImpl class. Injectors are created for constructors, fields and methods with @Inject annotation and do the actual work of injecting the resolved dependency. The injectors field is a Map, which is filled lazily, so it's enough to just remove the cached elements from it. To do that we first need to register a ReloadListener. In this case we chose InjectorImpl to implement it: class InjectorImpl implements Injector, ReloadListener {
...
public InjectorImpl() {
...
ReloaderFactory.getInstance().addReloadListener(this);
}
public void reloaded(Class klass) {
...
}
...
}The reloaded() method will now be called every time JavaRebel reloads a class. To empty the cache we should add the following: public void reloaded(Class klass) {
injectors.remove(klass);
}This will enable ALL newly created instances to have all the dependencies (including newly added ones) properly injected. However this will do nothing to existing instances and newly defined bindings will not be taken into account. Step 2: Loading new bindingsBindings are the way Google Guice manages relations between objects. While @Inject annotation tells it to inject the dependency, bindings define how to resolve it. E.g. the following code binds the interface Database to implementation class DatabaseImplTwo. binder().bind(Database.class).to(DatabaseImplTwo.class); The bindings fall into two categories:
We didn't find a good way to reload explicit bindings, since rerunning the binding code will actually lose all existing instances and have undefined side effects. However implicit bindings are resolved lazily, so we don't need to change anything for them to work. Since implicit bindings are encouraged by the Guice community the should be quite common. Step 3: Updating existing instancesThe main change we had to do to the Guice logics is reinjecting new dependencies to the existing configured instances. To do that we had to track the instances as they were created and reinject the dependencies on class change. We decided to track only singleton instances for the moment. Luckily Guice provides an injectMembers() method that reinject all dependencies to an existing instance. We decided to just rerun that when the class changes. To track the singleton we added the following code to the Scopes class where they are created: public class Scopes {
private static final Map singletonMap = new HashMap();
public static Object getSingleton(Class klass) {
return singletonMap.get(klass);
}
public static Map getSingletons() {
return new HashMap(singletonMap);
}
...
synchronized (Injector.class) {
if (instance == null) {
instance = creator.get();
singletonMap.put(key.getRawType(), instance);
}
}
...
}We track the singletons by their class. This code is flawed, since it assumes that there is only one Injector per application and that singleton instances are not garbage collected, but it will do for the example purposes. To make use of the tracked singletons we have to change the InjectorImpl.reloaded() method to do the following: public void reloaded(Class klass) {
injectors.remove(klass);
Object singleton = Scopes.getSingletons().get(klass);
if (singleton != null)
injectMembers(singleton);
}This will reinject dependencies to all singletons of the changed class. ConclusionDue to the way Guice is built up it was very easy to make it reload most changes to the dependencies. We used the Guice Examples project to test the reloading and it works very well for those (albeit small) examples. We provide both the patch and the patched distribution, so feel free to try them out. |
Sign in to add a comment
