Custom Guice Scope или лучший подход?
Здесь моя проблема:
Прежде всего важно знать, что я пишу симуляцию. Это автономное приложение и однопоточное. У меня есть по существу два класса объектов, которые имеют разные требования к области охвата.
-
Классы, которые должны использоваться в качестве синглонов на протяжении всего моделирования. Например, экземпляр Random.
-
Группы классов, созданные вместе, и внутри группы, каждый экземпляр должен обрабатываться как Singleton. Например, скажем, RootObject
- это класс верхнего уровня и имеет зависимость от ClassA
и ClassB
, оба из которых имеют зависимость от ClassD
. Для любого заданного RootObject
обе его зависимости (ClassA
и ClassB
) должны зависеть от того же экземпляра ClassD
. Однако экземпляры ClassD
не должны использоваться для разных экземпляров RootObject
.
Надеюсь, это имеет смысл. Я могу придумать два подхода к этому. Один из них заключается в том, чтобы пометить все введенные объекты как Singletons, создать инжектор корня и открутить дочерний инжектор каждый раз, когда мне нужно создать новый экземпляр RootObject
. Затем экземпляры RootObject
и всех его зависимостей создаются как синглтоны, но эта информация о масштабах выбрасывается в следующий раз, когда я иду, чтобы создать еще один RootObject
.
Второй подход - реализовать некоторый тип настраиваемой области.
Документация Guice дает противоречивые советы... С одной стороны, в ней говорится, что у вас должен быть один инжектор, и в идеале он один раз вызывается для создания класса высшего уровня. С другой стороны, он говорит, чтобы держаться подальше от пользовательских областей.
Ответы
Ответ 1
Мне кажется, что вам нужен объем для каждого экземпляра RootObject
и всех его зависимостей.
В Guice вы можете создать настраиваемую область, например @ObjectScoped
, например:
@Target({ TYPE, METHOD })
@Retention(RUNTIME)
@ScopeAnnotation
public @interface ObjectScoped {}
Теперь просто поместите RootObject
, A
, B
и D
в эту область:
@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
Теперь каждый RootObject
имеет свою собственную область. Вы можете реализовать это как простой 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);
}
}
Чтобы интегрировать эти области с Guice, вам понадобится com.google.inject.Scope
-implementation, которая позволяет вам переключаться между областями и соответствующей проводкой в 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;
}
}
Инициализируйте свою программу следующим образом:
ExampleModule module = new ExampleModule();
Injector injector = Guice.createInjector(module);
GuiceObjectScope objectScope = module.getObjectScope();
Создайте первый экземпляр RootObject
и его соответствующую область:
ObjectScope obj1 = new ObjectScope();
objectScope.enter(obj1);
RootObject rootObject1 = injector.getInstance(RootObject.class);
objectScope.leave();
Просто переключите область для второй группы объектов:
ObjectScope obj2 = new ObjectScope();
objectScope.enter(obj2);
RootObject rootObject2 = injector.getInstance(RootObject.class);
objectScope.leave();
Проверьте, соблюдены ли ваши требования:
assert rootObject1 != rootObject2;
assert rootObject1.getA() != rootObject2.getA();
assert rootObject1.getA().getD() == rootObject1.getB().getD();
assert rootObject1.getA().getD() != rootObject2.getB().getD();
Чтобы работать с группой объектов, просто введите его область действия и используйте инжектор:
objectScope.enter(obj1);
B b1 = injector.getInstance(B.class);
objectScope.leave();
assert rootObject1.getB() == b1;
Ответ 2
С небольшой настройкой, Guice может обеспечить двухуровневую область без специальной области. Внешний - @Singleton
, а внутренний - @RequestScoped
, предоставляемый расширением servlet
. Это работает, даже если вы говорите о чем-то, кроме контейнера сервлетов Java EE.
У вас есть один инжектор корневого уровня для обработки ваших синглетов. Обязательно объявите аннотацию области запроса в корневом уровне следующим образом:
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);
}
}
Если вы хотите ввести подпункт, вы выполните следующее:
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);
}
}
Мы используем ServletScopes.scopeRequest
для запуска анонимного Callable
внутри новой области запроса. Затем Callable
создает дочерний инжектор и добавляет новое связывание для любых объектов-объектов каждого запроса.
Семена - это объекты, которые нужны @RequestScoped
, но не могут быть созданы только Guice, например, запросами или идентификаторами итераций. Новый HashMap
, переданный как второй аргумент scopeRequest
, является еще одним способом буквально вставить семена в новую область. Я предпочитаю путь подмодуля, так как привязки для засеваемых значений всегда требуются в любом случае.
Затем дочерний инжектор "находится" в области запроса и может использоваться для предоставления @RequestScoped
вещей.
Смотрите также: Как использовать ServletScopes.scopeRequest() и ServletScopes.continueRequest()?
Ответ 3
Рассматривали ли вы использование провайдера? Было бы легко написать то, которое соответствует вашим требованиям, например:
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, ...);
}
}
Вы можете использовать провайдер двумя способами:
- Привяжите его как поставщика с интерфейсом
@Provides
или декорированием привязки .toProvider()
.
- Ввести поставщика напрямую и вызвать его для создания экземпляров
RootObject
по мере необходимости.
Надеюсь, что это поможет.
Ответ 4
Могу ли я спросить, почему вам нужны синглеты?
Я бы не рекомендовал создавать настраиваемую область. Самый лучший и простой способ смешивания областей - это внедрить поставщиков вместо объектов. С поставщиками вы можете управлять областью вашего объекта из логики бизнес-кода.
Подробнее см. в документации Guice.