What's new? | Help | Directory | Sign in
Google
                
Search
for
Updated Jun 06, 2008 by ekabanov
GuiceZeroTurnaroundConversion  
An example conversion of Guice to enable Zero Turnaround development on top of JavaRebel SDK.

Introduction

This 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:

  • JavaRebel supports annotation reloading since 1.1 M1 (and they just show up in usual reflection API)
  • Annotations are local to the class, which means we can just monitor class for changes and react by reparsing annotations, not some XML
  • It is well designed, so it's clear what to do
  • It is not a trivial case, since you have to inject new dependencies.

Step 1: Resetting the reflection cache

Almost 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 bindings

Bindings 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 instances

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

Conclusion

Due 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