问题 如何使用Guice @Inject到现有的对象层次结构?


我有一个现有的对象层次结构,其中一些对象具有需要注入的字段。还有一些使用构造的其他对象 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) 对于层次结构中的根对象,注入所有感兴趣的对象。

不是很好的解决方案,但一方面完成工作。另一方面,我需要从此层次结构中注入对象。我想这可以通过为这些对象实现自定义提供程序来完成。

我的问题有更好的解决方案吗?也许我的方法也有问题?


8194
2018-05-18 09:35


起源



答案:


这个解决方案可行,但我想提出一个稍微不同的解决方案。

具体来说,由于您将遍历深层对象结构,因此这看起来像访问者模式的工作。此外,您所描述的似乎要求两阶段注入器:一个“引导”阶段,可以注入透视创建的层次结构所需的东西(但不能注入任何透视创建的元素)和第二阶段这是您的应用程序使用的真正注入器(可以注入任何东西)。

我建议的是这个基本模式:让访问者遍历层次结构,并且随着它的进展,它会注入那些需要它的东西并记录那些需要注入其他地方的东西。然后,当它完成访问所有内容时,它会使用 Injector.createChildInjector 做一个新的 Injector 这可以从原件中注入东西 Injector 和来自枢轴创建的层次结构的东西。

首先定义一个可以访问此层次结构中所有内容的访问者

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

然后为所有pivot创建的元素定义一个接口:

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);
}

12
2018-05-18 11:44



哇!这是非常彻底和精心的答案。我非常感激。并感谢分享Guice魔术:)我会尝试建议的方法。 (明天将接受答案。) - dragonfly


答案:


这个解决方案可行,但我想提出一个稍微不同的解决方案。

具体来说,由于您将遍历深层对象结构,因此这看起来像访问者模式的工作。此外,您所描述的似乎要求两阶段注入器:一个“引导”阶段,可以注入透视创建的层次结构所需的东西(但不能注入任何透视创建的元素)和第二阶段这是您的应用程序使用的真正注入器(可以注入任何东西)。

我建议的是这个基本模式:让访问者遍历层次结构,并且随着它的进展,它会注入那些需要它的东西并记录那些需要注入其他地方的东西。然后,当它完成访问所有内容时,它会使用 Injector.createChildInjector 做一个新的 Injector 这可以从原件中注入东西 Injector 和来自枢轴创建的层次结构的东西。

首先定义一个可以访问此层次结构中所有内容的访问者

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

然后为所有pivot创建的元素定义一个接口:

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);
}

12
2018-05-18 11:44



哇!这是非常彻底和精心的答案。我非常感激。并感谢分享Guice魔术:)我会尝试建议的方法。 (明天将接受答案。) - dragonfly


您可以注入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);
  }
}

0
2018-05-18 15:51



此解决方案的问题在于使用此技术意味着层次结构中的所有对象都是使用Guice构造的。事实并非如此。而且我猜使用 MembersInjector<T> 如果使用Guice注入Car实例,则引擎实例将被正常注入,因此这里是过多的。也过去了 Injector (要么 MembersInjector 就此而言)通过对象树不是一个正常的用例。 - dragonfly