Custom Guice Scope, or a better approach?

2019-01-22 03:11发布

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.

4条回答
祖国的老花朵
2楼-- · 2019-01-22 03:31

May I ask why do you need to have singletons ?

I would not recommend to create custom scope. The best and easiest way to mix scopes is to inject providers instead of objects. With the providers you can control the scope of your object from you business code logic.

See this Guice documentation for details.

查看更多
我欲成王,谁敢阻挡
3楼-- · 2019-01-22 03:32

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;
查看更多
相关推荐>>
4楼-- · 2019-01-22 03:41

Have you considered using a provider? It would be easy to write one that meets your requirements, e.g.:

import com.google.inject.Provider

class RootObjectProvider implements Provider<RootObject> {

    ...

    @Override
    RootObject get() {
        ClassD d = new ClassD( .... );
        ClassB b = new ClassB( ..., d, ...);
        ClassC c = new ClassC( ..., d, ...); // Note that b and c share d.
        return new RootObject(b, c, ...);
    }
}

You can use the provider two ways:

  1. Bind it as a provider with either the @Provides interface or the .toProvider() binding decoration.
  2. Inject the provider directly and invoke it to create RootObject instances as needed.

Hope that this helps.

查看更多
闹够了就滚
5楼-- · 2019-01-22 03:43

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()?

查看更多
登录 后发表回答