Как вставить в существующую иерархию объектов с помощью Guice?

У меня есть существующая иерархия объектов, где у некоторых объектов есть поля, которые необходимо ввести. Также есть некоторые другие объекты, которые создаются с помощью Google Guice и должны быть добавлены ссылки на некоторые объекты из ранее описанной иерархии объектов. Как мне сделать такую ​​инъекцию с помощью Guice?

Проблема в том, что объекты из существующей иерархии не были построены с использованием Guice и поэтому не подлежат процессу инжекции по умолчанию. Существует, конечно, метод injector.injectMembers(), способный вставлять в существующий экземпляр объекта, но он не работает с иерархиями объектов.

Для тех, кто задается вопросом, почему я не могу построить упомянутую иерархию объектов с помощью Guice. Эта иерархия представляет объекты GUI и построена с помощью GUI-инфраструктуры (Apache Pivot) из описания декларативного GUI (на самом деле этот процесс можно охарактеризовать как объект десериализации). В этом случае конструкция интерфейса довольно проста, и я хочу только добавить определенные ссылки на службы в объекты интерфейса и наоборот (для обратных вызовов).

Подход, который я сейчас собираюсь сделать, описан ниже.

Для ввода в ранее существовавшую иерархию объектов просто пусть все объекты, интересующиеся инъекцией, реализуют определенный интерфейс, например:

public interface Injectable {
  void injectAll(Injector injector);
}

Эти объекты затем реализуют этот интерфейс следующим образом:

public void injectAll(Injector injector) {
  injector.injectMembers(this);
  for (Injectable child : children)
    child.injectAll(injector);
}

Затем я просто вызываю mainWindow.injectAll(injector) для корневого объекта в иерархии, и все объекты, представляющие интерес, вводятся.

Не очень приятное решение, но выполняет работу с одной стороны. С другой стороны, мне нужно вводить объекты из этой иерархии. Я предполагаю, что это можно сделать через реализацию настраиваемого поставщика для таких объектов.

Есть ли лучшее решение моей проблемы? Может быть, что-то не так с моим подходом?

Ответ 1

Это решение будет работать, но я хотел бы предложить вам немного другое.

В частности, поскольку вы собираетесь пересекать структуру глубоких объектов, это действительно выглядит как работа для шаблона посетителя. Кроме того, то, что вы описываете, похоже, вызывает двухступенчатый инжектор: стадия "начальной загрузки", которая может вводить материал, необходимый для созданной с помощью сводной иерархии (но не может вводить элементы, созданные с помощью поворота), и второй этап это реальный инжектор, используемый вашим приложением (который может вводить что угодно).

Я бы предположил, что это базовый шаблон: сделать посетителя, который проходит иерархию, и, как говорится, он делает инъекцию на те вещи, которые в ней нуждаются, и записывает те вещи, которые нужно вводить в другом месте. Затем, когда это делается, посетив все, он использует Injector.createChildInjector, чтобы создать новый Injector, который может вставлять материал из оригинального Injector и прочее из созданной по иерархии иерархии.

Сначала определите посетителя, который может поразить все в этой иерархии:

public interface InjectionVisitor {
  void needsInjection(Object obj);
  <T> void makeInjectable(Key<T> key, T instance);
}

Затем определите интерфейс для всех созданных с помощью элементов:

public interface InjectionVisitable {
  void acceptInjectionVisitor(InjectionVisitor visitor);
}

Вы реализуете этот интерфейс в своих созданных с помощью ключа классах (считая этот код в классе FooContainer):

public void acceptInjectionVisitor(InjectionVisitor visitor) {
  visitor.needsInjection(this);
  visitor.makeInjectable(Key.get(FooContainer.class), this);
  for (InjectionVisitable child : children) {
    child.acceptInjectionVisitor(visitor);
  }
}

Обратите внимание, что первые два оператора являются необязательными - возможно, некоторые объекты в иерархической иерархии не нуждаются в инъекции, и также может быть, что некоторые из них вы не захотите вводить позже. Также обратите внимание на использование Key - это означает, что если вы хотите, чтобы какой-либо класс был инъекционным с определенной аннотацией, вы можете сделать что-то вроде:

visitor.makeInjectable(Key.get(Foo.class, Names.named(this.getName())), this);

Теперь, как вы реализуете InjectionVisitor? Вот как:

public class InjectionVisitorImpl implements InjectionVisitor {
  private static class BindRecord<T> {
    Key<T> key;
    T value;
  }

  private final List<BindRecord<?>> bindings = new ArrayList<BindRecord<?>>();
  private final Injector injector;

  public InjectionVisitorImpl(Injector injector) {
    this.injector = injector;
  }

  public void needsInjection(Object obj) {
    injector.injectMemebers(obj);
  }

  public <T> void makeInjectable(Key<T> key, T instance) {
    BindRecord<T> record = new BindRecord<T>();
    record.key = key;
    record.value = instance;
    bindings.add(record);
  }

  public Injector createFullInjector(final Module otherModules...) {
    return injector.createChildInjector(new AbstractModule() {
      protected void configure() {
        for (Module m : otherModules) { install(m); }
        for (BindRecord<?> record : bindings) { handleBinding(record); }
      }
      private <T> handleBinding(BindRecord<T> record) {
        bind(record.key).toInstance(record.value);
      }
    });
  }
}

Затем вы используете это в своем методе main как:

PivotHierarchyTopElement top = ...; // whatever you need to do to make that
Injector firstStageInjector = Guice.createInjector(
   // here put all the modules needed to define bindings for stuff injected into the
   // pivot hierarchy.  However, don't put anything for stuff that needs pivot
   // created things injected into it.
);
InjectionVisitorImpl visitor = new InjectionVisitorImpl(firstStageInjector);
top.acceptInjectionVisitor(visitor);
Injector fullInjector = visitor.createFullInjector(
  // here put all your other modules, including stuff that needs pivot-created things
  // injected into it.
);
RealMainClass realMain = fullInjector.getInstance(RealMainClass.class);
realMain.doWhatever();

Обратите внимание, что способ createChildInjector гарантирует, что если у вас есть какие-либо объекты @Singleton, связанные с материалом, внедренным в иерархию сводных данных, вы получите те же экземпляры, которые инъецируются вашим реальным инжектором - делегат fullInjector будет делегировать инжекции в firstStageInjector, пока firstStageInjector способен обрабатывать инъекцию.

Отредактировано для добавления: Интересное расширение этого (если вы хотите углубиться в магию Guice) - это изменить InjectionImpl, чтобы он записывал место в исходном коде, который назывался makeInjectable. Это позволяет вам получать сообщения об ошибках из Guice, когда ваш код случайно сообщает посетителю о двух разных вещах, связанных с одним и тем же ключом. Для этого вам нужно добавить StackTraceElement в BindRecord, записать результат new RuntimeException().getStackTrace()[1] внутри метода makeInjectable, а затем изменить handleBinding на:

private <T> handleBinding(BindRecord<T> record) {
  binder().withSource(record.stackTraceElem).bind(record.key).toInstance(record.value);
}

Ответ 2

Вы можете ввести MembersInjectors для ввода вложенных полей. Например, это глубоко вдохнет существующий экземпляр Car:

public class Car {
  Radio radio;
  List<Seat> seats;
  Engine engine;

  public Car(...) {...}

  @Inject void inject(RadioStation radioStation,
      MembersInjector<Seat> seatInjector,
      MembersInjector<Engine> engineInjector) {
    this.radio.setStation(radioStation);
    for (Seat seat : seats) {
      seatInjector.injectMembers(seat);
    }
    engineInjector.injectMembers(engine);
  }
}

public class Engine {
  SparkPlug sparkPlug;
  Turbo turbo

  public Engine(...) {...}

  @Inject void inject(SparkPlug sparkplug,
      MembersInjector<Turbo> turboInjector) {
    this.sparkPlug = sparkPlug;
    turboInjector.injectMembers(turbo);
  }
}