Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom Guice Scope, or a better approach?

Tags:

Here's my problem:

It's first important to know that I'm writing a simulation. This is a standalone application, and is single-threaded. I have essentially two classes of objects that have different scoping requirements.

  1. Classes that should be used as singletons throughout the entire simulation. An instance of Random, as an example.

  2. Groups of classes that are created together, and within the group, each instance should be treated like a Singleton. For example, say RootObject is the top level class, and has a dependency to ClassA and ClassB, both of which have a dependency to ClassD. For any given RootObject, both of its dependencies (ClassA and ClassB) should depend on the same instance of ClassD. However, instances of ClassD should not be shared across different instances of RootObject.

Hopefully that makes sense. I can think of two approaches to this. One is to mark all of the injected objects as Singletons, create the root injector, and spin off a child injector each time I need to create a new RootObject instance. Then, the instances of RootObject and all of its dependencies are created as Singletons, but that scoping information is thrown away the next time I go to create another RootObject.

The second approach is to implement some type of custom scope.

The Guice documentation gives conflicting advice... On one hand, it says that you should have a single injector, and that ideally it is called once to create some top level class. On the other hand, it says to stay away from custom scopes.

like image 470
Josh Avatar asked Mar 30 '12 12:03

Josh


People also ask

What is eager singleton in Guice?

Guice provides special syntax for making an object that has the singleton scope, and is initialized to eager mode rather than lazy mode. Following is the syntax: bind(NotificationService. class).to(SMSService.

Is Guice a provider singleton?

Guice comes with a built-in @Singleton scope that reuses the same instance during the lifetime of an application within a single injector.

What does @inject mean Guice?

In Guice, the @Inject annotation will be used to define setter-based as well as constructor-based dependency injection. An instance of this class is utilized to send notifications by means of the accessible correspondence services.

What does @provides do in Java?

@Provides Methods When you need code to create an object, use an @Provides method. The method must be defined within a module, and it must have an @Provides annotation. The method's return type is the bound type. Whenever the injector needs an instance of that type, it will invoke the method.


2 Answers

It seems to me like you need a scope for each instance of RootObject and all its dependencies.

In Guice you can create a custom scope, say @ObjectScoped, like this:

@Target({ TYPE, METHOD }) @Retention(RUNTIME) @ScopeAnnotation public @interface ObjectScoped {} 

Now just place RootObject, A, B and D into this scope:

@ObjectScoped public class RootObject {      private A a;     private B b;      @Inject     public RootObject(A a, B b) {         this.a = a;         this.b = b;     }      public A getA() {         return a;     }      public B getB() {         return b;     }  }  @ObjectScoped public class A {      private D d;      @Inject     public A(D d) {         this.d = d;     }      public D getD() {         return d;     } }  // The same for B and D 

Now each RootObject has its own scope. You can implement this as a simple HashMap:

public class ObjectScope {      private Map<Key<?>,Object> store = new HashMap<Key<?>,Object>();      @SuppressWarnings("unchecked")     public <T> T get(Key<T> key) {         return (T)store.get(key);     }      public <T> void set(Key<T> key, T instance) {         store.put(key, instance);     }  } 

To integrate these scopes with Guice you will need a com.google.inject.Scope-implementation which lets you switch the scopes and the corresponding wiring in your Module.

public class GuiceObjectScope implements Scope {      // Make this a ThreadLocal for multithreading.     private ObjectScope current = null;      @Override     public <T> Provider<T> scope(final Key<T> key, final Provider<T> unscoped) {         return new Provider<T>() {              @Override             public T get() {                  // Lookup instance                 T instance = current.get(key);                 if (instance==null) {                      // Create instance                     instance = unscoped.get();                     current.set(key, instance);                 }                 return instance;              }         };     }      public void enter(ObjectScope scope) {         current = scope;     }      public void leave() {         current = null;     }  }  public class ExampleModule extends AbstractModule {      private GuiceObjectScope objectScope = new GuiceObjectScope();      @Override     protected void configure() {         bindScope(ObjectScoped.class, objectScope);         // your bindings     }      public GuiceObjectScope getObjectScope() {         return objectScope;     }  } 

Initialize your program like this:

ExampleModule module = new ExampleModule(); Injector injector = Guice.createInjector(module); GuiceObjectScope objectScope = module.getObjectScope(); 

Create the first instance of RootObject and its corresponding scope:

ObjectScope obj1 = new ObjectScope(); objectScope.enter(obj1); RootObject rootObject1 = injector.getInstance(RootObject.class); objectScope.leave(); 

Just switch the scope for a second group of objects:

ObjectScope obj2 = new ObjectScope(); objectScope.enter(obj2); RootObject rootObject2 = injector.getInstance(RootObject.class); objectScope.leave(); 

Test if your requirements are met:

assert rootObject1 != rootObject2; assert rootObject1.getA() != rootObject2.getA(); assert rootObject1.getA().getD() == rootObject1.getB().getD(); assert rootObject1.getA().getD() != rootObject2.getB().getD(); 

To work with a group of objects just enter its scope and use the injector:

objectScope.enter(obj1); B b1 = injector.getInstance(B.class); objectScope.leave(); assert rootObject1.getB() == b1; 
like image 144
electrodraco Avatar answered Oct 14 '22 19:10

electrodraco


With a little setup, Guice can provide two-tier scoping without a custom scope. The outer one is @Singleton, and the inner is @RequestScoped, provided by the servlet extension. This works even if you're talking about something other than a Java EE servlet container.

Have a single, root-level injector to handle your singletons. Be sure to declare the request scope annotation in your root-level module as so:

public class RootModule extends AbstractModule {   @Override   protected void configure() {     // Tell guice about the request scope, so that we can use @RequestScoped     bindScope(RequestScoped.class, ServletScopes.REQUEST);   } } 

When you want to enter a sub-scope, you do this:

private void scopeAndInject(final Object perRequestSeed) {   try {     ServletScopes.scopeRequest(new Callable<Void>() {       public Void call() {         Injector requestScoped = getRootInjector().createChildInjector(           new AbstractModule() {             @Override             protected void configure() {               bind(Object.class).toInstance(perRequestSeed);             }           }         );          requestScoped.get(Something.class);          return null;       }     }, new HashMap<Key<?>, Object>()).call();   } catch (Exception e) {     throw new RuntimeException(e);   } } 

What we're doing here is using ServletScopes.scopeRequest to run the anonymous Callable inside a new request scope. The Callable then creates a child injector and adds a new binding for any per-request seed objects.

The seeds are objects that @RequestScoped things would need but couldn't be created by Guice alone, like requests or iteration IDs. The new HashMap passed as the second argument to scopeRequest is another way to literally insert seeds into the new scope. I prefer the submodule way, since the bindings for the seeded values are always required anyway.

The child injector is then "in" the request scope and can be used to provide @RequestScoped things.

See this also: How to use ServletScopes.scopeRequest() and ServletScopes.continueRequest()?

like image 27
phs Avatar answered Oct 14 '22 20:10

phs