在 Guice 中避免框架强加的循环依赖
Avoiding framework-imposed circular dependencies in Guice
请注意:虽然这个问题特别提到了 Swing,但我相信这是一个纯粹的 Guice ( 4.0) 核心问题,突出了 Guice 和其他自以为是的框架的潜在通用问题。
在 Swing 中,您有自己的应用程序 UI,它扩展了 JFrame
:
// Pseudo-code
class MyApp extends JFrame {
// ...
}
您的应用 (JFrame
) 需要一个菜单栏:
// Pseudo-code
JMenuBar menuBar = new JMenuBar()
JMenu fileMenu = new JMenu('File')
JMenu manageMenu = new JMenu('Manage')
JMenuItem widgetsSubmenu = new JMenuItem('Widgets')
manageMenu.add(widgetsSubmenu)
menuBar.add(fileMenu)
menuBar.add(manageMenu)
return menuBar
并且您的菜单项需要动作侦听器,其中许多会更新您应用的 contentPane
:
widgetsSubmenu.addActionListener(new ActionListener() {
@Override
void actionPerformed(ActionEvent actionEvent) {
// Remove all the elements from the main contentPane
yourApp.contentPane.removeAll()
// Now add a new panel to the contentPane
yourApp.contentPane.add(someNewPanel)
}
})
因此本质上存在循环依赖:
- 您的应用/
JFrame
需要一个 JMenuBar
实例
- 您的
JMenuBar
需要 0+ JMenus
和 JMenuItems
- 您的
JMenuItems
(好吧,那些会更新您的 JFrame
's contentPane
' 的)需要动作侦听器
- 然后,为了更新
JFrame
的 contentPane
,这些动作侦听器需要引用您的 JFrame
参加以下模块:
// Pseudo-code
class MyModule extends AbstractModule {
@Override
void configure() {
// ...
}
@Provides
MyApp providesMyApp(JMenuBar menuBar) {
// Remember MyApp extends JFrame...
return new MyApp(menuBar, ...)
}
@Provides
JMenuBar providesMenuBar(@Named('widgetsListener') ActionListener widgetsMenuActionListener) {
JMenuBar menuBar = new JMenuBar()
JMenu fileMenu = new JMenu('File')
JMenu manageMenu = new JMenu('Manage')
JMenuItem widgetsSubmenu = new JMenuItem('Widgets')
widgetsSubmenu.addActionListener(widgetsMenuActionListener)
manageMenu.add(widgetsSubmenu)
menuBar.add(fileMenu)
menuBar.add(manageMenu)
return menuBar
}
@Provides
@Named('widgetsListener')
ActionListener providesWidgetsActionListener(Myapp myApp, @Named('widgetsPanel') JPanel widgetsPanel) {
new ActionListener() {
@Override
void actionPerformed(ActionEvent actionEvent) {
// Here is the circular dependency. MyApp needs an instance of this listener to
// to be instantiated, but this listener depends on a MyApp instance in order to
// properly update the main content pane...
myApp.contentPane.removeAll()
myApp.contentPane.add(widgetsPanel)
}
}
}
}
这会在运行时产生循环依赖错误,例如:
Exception in thread "AWT-EventDispatcher" com.google.inject.ProvisionException: Unable to provision, see the following errors:
1) Tried proxying com.me.myapp.MyApp to support a circular dependency, but it is not an interface.
while locating com.me.myapp.MyApp
所以我问:有什么方法可以避免这种情况? Guice 是否有 API 或扩展库来处理此类问题?有没有办法重构代码来打破循环依赖?其他解决方案?
更新
请查看我在 GitHub 上的 guice-swing-example SSCCE 项目。
好吧,如果你确实有循环依赖,它就不能是创建循环依赖——否则你根本无法创建对象循环。
您可以通过在模块上请求注入来执行 post-创建配置。例如,您可以使用它来将 JMenuBar
添加到 JFrame
:
class MyModule extends AbstractModule {
@Override void configure() {
requestInjection(this);
}
@Inject void addMenuToFrame(JFrame frame, JMenuBar menu) {
frame.setMenuBar(menu);
}
}
我没有测试它是否有效(因为你没有提供足够的代码来尝试它) - 所以你可能需要试验在正确的地方打破当前循环的顺序随后像这样加入它 - 但像这样的东西应该适合你。
我已经回答了一些关于像这样注入 guice 模块的其他问题 - maybe have a look at those too,因为其他人对此也有一些很好的提示,而不是我在这里尝试但未能正确重复它们.
为了帮助我理解,我一直在研究这个问题,我发现您需要将 JFrame
和 JMenuBar
绑定在单例范围内才能正常工作很有用,因为方法注入是在创建注入器时发生的,而不是在随后创建 JFrame
和 JMenuBar
实例时发生的(可能很明显)。
我在回答这个问题时无法根据您的代码测试答案,因为我不知道(而且我不想学习)Groovy。
但基本上,当你有循环依赖时,打破它的最好方法是使用 Provider
.
package so36042838;
import com.google.common.base.*;
import com.google.inject.Guice;
import javax.inject.*;
public class Question {
@Singleton public final static class A {
B b;
@Inject A(B b) { this.b = b; }
void use() { System.out.println("Going through"); }
}
@Singleton public final static class B {
A a;
@Inject B(A a) { this.a = a; }
void makeUseOfA() { a.use(); }
}
public static void main(String[] args) {
Guice.createInjector().getInstance(B.class).makeUseOfA(); // Produces a circular dependency error.
}
}
然后可以这样重写:
package so36042838;
import com.google.common.base.*;
import com.google.inject.Guice;
import javax.inject.*;
public class Question {
@Singleton public final static class A {
B b;
@Inject A(B b) { this.b = b; }
void use() { System.out.println("Going through"); }
}
@Singleton public final static class B {
Supplier<A> a;
@Inject B(Provider<A> a) { this.a = Suppliers.memoize(a::get); } // prints "Going through" as expected
void makeUseOfA() { a.get().use(); }
}
public static void main(String[] args) {
Guice.createInjector().getInstance(B.class).makeUseOfA();
}
}
有多种技术可用于重构循环依赖,使其不再是循环的,并且 Guice docs recommend doing that whenever possible。当这不可能时,错误消息 Guice 提供了 Guice 解决此问题的一般方法的提示:使用接口将您的 API 与您的实现分开。
严格来说只需要对圈子中的一个class做这件事,但对大多数或每个class做这件事可能会提高模块化和编写测试代码的便利性。创建一个接口,MyAppInterface
,在其中声明您的动作侦听器需要直接调用的每个方法(getContentPane()
似乎是您发布的代码中唯一的一个),并让 MyApp
实现它。然后在您的模块配置中将 MyAppInterface
绑定到 MyApp
(在您的情况下,只需更改 providesMyApp
的 return 类型即可),声明动作侦听器提供者采取 MyAppInterface
,和 运行 它。您可能还需要将代码中的其他一些地方从 MyApp
切换到 MyAppInterface
,具体取决于详细信息。
这样做将允许 Guice 本身为您打破循环依赖。当它看到循环依赖时,它将生成一个新的零依赖实现 class of MyAppInterface
作为代理包装器,将其实例传递给动作侦听器提供者,填写依赖链从那里直到它可以创建一个真正的 MyApp
对象,然后它会将 MyApp
对象粘贴到 Guice 生成的对象中,该对象将转发所有方法调用给它。
虽然我在上面使用了 MyAppInterface
,但更常见的命名模式是实际使用接口的 MyApp
名称并将现有的 MyApp
class 重命名为MyAppImpl
.
请注意,Guice 的自动循环依赖中断方法要求您在初始化完成之前不要对生成的代理对象调用任何方法,因为在那之前它不会有包装对象将它们转发到。
编辑:我还没有 Groovy 或 Gradle 准备好尝试 运行 你的 SSCCE 进行测试,但我认为你已经非常接近打破这个圈子了。用 @Singleton
注释 DefaultFizzClient
class 和每个 @Provides
方法,并从 provideExampleApp
中删除 menuBar
参数,我认为应该让它发挥作用。
@Singleton
:class 或提供者方法应标记为 @Singleton
,当只应创建它的单个实例时。当它被注入多个不同的地方或被多次请求时,这一点很重要。使用 @Singleton
,Guice 创建一个实例,保存对它的引用,并每次都使用该实例。如果没有,它会为每个引用创建一个新的单独实例。从概念上讲,这是定义 "how to make an X" 还是 "this here is the X" 的问题。在我看来,您正在创建的 UI 元素属于后一类 - 您的应用程序的单一菜单栏,而不是任何任意菜单栏等。
删除menuBar
:您已经注释掉了该方法中使用它的唯一一行。只需从方法声明中删除参数即可。至于如何让菜单栏进入应用程序,这已经由 addMenuToFrame
方法结合 requestInjection(this)
调用处理。
如果进行这些更改,运行 Guice 将通过的逻辑可能会有所帮助:
- 当您使用
ExampleAppModule
创建注入器时,它会调用模块的 configure()
方法。这会设置一些绑定并告诉 Guice,无论何时完成所有绑定设置,它都应该扫描 ExampleAppModule
实例以查找用 @Inject
注释的字段和方法并填写它们。
configure()
returns 并且,绑定设置完成后,Guice 接受 requestInjection(this)
调用。扫描发现addMenuToFrame
被注解为@Inject
。检查该方法,Guice 发现需要一个 ExampleApp
实例和一个 JMenuBar
实例才能调用它。
- Guice 寻找创建
ExampleApp
实例的方法并找到 provideExampleApp
方法。 Guice 检查该方法并发现需要一个 FizzClient
实例来调用它。
- Guice 寻找创建
FizzClient
实例的方法并找到 class 绑定到 DefaultFizzClient
。 DefaultFizzClient
有一个默认的无参数构造函数,所以 Guice 只是调用它来获取一个实例。
- 获得
FizzClient
实例后,Guice 现在可以调用 provideExampleApp
。它这样做,从而获得一个 ExampleApp
实例。
- Guice 仍然需要一个
JMenuBar
才能调用 addMenuToFrame
,因此它寻找一种方法来创建一个并找到 providesMenuBar
方法。 Guice 检查该方法并指出它需要一个名为 "widgetsMenuActionListener". 的 ActionListener
- Guice 寻找一种方法来创建名为 "widgetsMenuActionListener" 的
ActionListener
并找到 providesWidgetsMenuActionListener
方法。 Guice 检查该方法并发现它需要一个 ExampleApp
实例和一个名为 "widgetsPanel" 的 JPanel
。它已经有一个 ExampleApp
实例,在第 5 步中获得,并且它从标记为 @Singleton
的东西中获得它,所以它重用相同的实例而不是再次调用 provideExampleApp
。
- Guice 寻找一种方法来创建名为 "widgetsPanel" 的
JPanel
并找到 providesWidgetPanel
方法。这个方法没有参数,所以Guice只是调用它并获取它需要的JPanel
。
- 除了已经创建的
ExampleApp
之外,还获得了一个具有正确名称的 JPanel
,Guice 调用 providesWidgetsMenuActionListener
,从而获得一个命名的 ActionListener
实例。
- 获得了具有正确名称的
ActionListener
后,Guice 调用 providesMenuBar
,从而获得了 JMenuBar
实例。
- 终于,终于,获得了一个
ExampleApp
实例和一个 JMenuBar
实例,Guice 调用 addMenuToFrame
,将菜单添加到框架。
- 现在,在所有这些发生之后,您在
main
中的 getInstance(ExampleApp)
调用将被执行。 Guice 检查,发现它已经有一个来自标记为 @Singleton
的源的 ExampleApp
实例,并且 return 是那个实例。
看完所有这些,似乎 @Singleton
仅在 provideExampleApp
上是绝对必要的,但我认为对其他一切也有意义。
请注意:虽然这个问题特别提到了 Swing,但我相信这是一个纯粹的 Guice ( 4.0) 核心问题,突出了 Guice 和其他自以为是的框架的潜在通用问题。
在 Swing 中,您有自己的应用程序 UI,它扩展了 JFrame
:
// Pseudo-code
class MyApp extends JFrame {
// ...
}
您的应用 (JFrame
) 需要一个菜单栏:
// Pseudo-code
JMenuBar menuBar = new JMenuBar()
JMenu fileMenu = new JMenu('File')
JMenu manageMenu = new JMenu('Manage')
JMenuItem widgetsSubmenu = new JMenuItem('Widgets')
manageMenu.add(widgetsSubmenu)
menuBar.add(fileMenu)
menuBar.add(manageMenu)
return menuBar
并且您的菜单项需要动作侦听器,其中许多会更新您应用的 contentPane
:
widgetsSubmenu.addActionListener(new ActionListener() {
@Override
void actionPerformed(ActionEvent actionEvent) {
// Remove all the elements from the main contentPane
yourApp.contentPane.removeAll()
// Now add a new panel to the contentPane
yourApp.contentPane.add(someNewPanel)
}
})
因此本质上存在循环依赖:
- 您的应用/
JFrame
需要一个JMenuBar
实例 - 您的
JMenuBar
需要 0+JMenus
和JMenuItems
- 您的
JMenuItems
(好吧,那些会更新您的JFrame
'scontentPane
' 的)需要动作侦听器 - 然后,为了更新
JFrame
的contentPane
,这些动作侦听器需要引用您的JFrame
参加以下模块:
// Pseudo-code
class MyModule extends AbstractModule {
@Override
void configure() {
// ...
}
@Provides
MyApp providesMyApp(JMenuBar menuBar) {
// Remember MyApp extends JFrame...
return new MyApp(menuBar, ...)
}
@Provides
JMenuBar providesMenuBar(@Named('widgetsListener') ActionListener widgetsMenuActionListener) {
JMenuBar menuBar = new JMenuBar()
JMenu fileMenu = new JMenu('File')
JMenu manageMenu = new JMenu('Manage')
JMenuItem widgetsSubmenu = new JMenuItem('Widgets')
widgetsSubmenu.addActionListener(widgetsMenuActionListener)
manageMenu.add(widgetsSubmenu)
menuBar.add(fileMenu)
menuBar.add(manageMenu)
return menuBar
}
@Provides
@Named('widgetsListener')
ActionListener providesWidgetsActionListener(Myapp myApp, @Named('widgetsPanel') JPanel widgetsPanel) {
new ActionListener() {
@Override
void actionPerformed(ActionEvent actionEvent) {
// Here is the circular dependency. MyApp needs an instance of this listener to
// to be instantiated, but this listener depends on a MyApp instance in order to
// properly update the main content pane...
myApp.contentPane.removeAll()
myApp.contentPane.add(widgetsPanel)
}
}
}
}
这会在运行时产生循环依赖错误,例如:
Exception in thread "AWT-EventDispatcher" com.google.inject.ProvisionException: Unable to provision, see the following errors:
1) Tried proxying com.me.myapp.MyApp to support a circular dependency, but it is not an interface.
while locating com.me.myapp.MyApp
所以我问:有什么方法可以避免这种情况? Guice 是否有 API 或扩展库来处理此类问题?有没有办法重构代码来打破循环依赖?其他解决方案?
更新
请查看我在 GitHub 上的 guice-swing-example SSCCE 项目。
好吧,如果你确实有循环依赖,它就不能是创建循环依赖——否则你根本无法创建对象循环。
您可以通过在模块上请求注入来执行 post-创建配置。例如,您可以使用它来将 JMenuBar
添加到 JFrame
:
class MyModule extends AbstractModule {
@Override void configure() {
requestInjection(this);
}
@Inject void addMenuToFrame(JFrame frame, JMenuBar menu) {
frame.setMenuBar(menu);
}
}
我没有测试它是否有效(因为你没有提供足够的代码来尝试它) - 所以你可能需要试验在正确的地方打破当前循环的顺序随后像这样加入它 - 但像这样的东西应该适合你。
我已经回答了一些关于像这样注入 guice 模块的其他问题 - maybe have a look at those too,因为其他人对此也有一些很好的提示,而不是我在这里尝试但未能正确重复它们.
为了帮助我理解,我一直在研究这个问题,我发现您需要将 JFrame
和 JMenuBar
绑定在单例范围内才能正常工作很有用,因为方法注入是在创建注入器时发生的,而不是在随后创建 JFrame
和 JMenuBar
实例时发生的(可能很明显)。
我在回答这个问题时无法根据您的代码测试答案,因为我不知道(而且我不想学习)Groovy。
但基本上,当你有循环依赖时,打破它的最好方法是使用 Provider
.
package so36042838;
import com.google.common.base.*;
import com.google.inject.Guice;
import javax.inject.*;
public class Question {
@Singleton public final static class A {
B b;
@Inject A(B b) { this.b = b; }
void use() { System.out.println("Going through"); }
}
@Singleton public final static class B {
A a;
@Inject B(A a) { this.a = a; }
void makeUseOfA() { a.use(); }
}
public static void main(String[] args) {
Guice.createInjector().getInstance(B.class).makeUseOfA(); // Produces a circular dependency error.
}
}
然后可以这样重写:
package so36042838;
import com.google.common.base.*;
import com.google.inject.Guice;
import javax.inject.*;
public class Question {
@Singleton public final static class A {
B b;
@Inject A(B b) { this.b = b; }
void use() { System.out.println("Going through"); }
}
@Singleton public final static class B {
Supplier<A> a;
@Inject B(Provider<A> a) { this.a = Suppliers.memoize(a::get); } // prints "Going through" as expected
void makeUseOfA() { a.get().use(); }
}
public static void main(String[] args) {
Guice.createInjector().getInstance(B.class).makeUseOfA();
}
}
有多种技术可用于重构循环依赖,使其不再是循环的,并且 Guice docs recommend doing that whenever possible。当这不可能时,错误消息 Guice 提供了 Guice 解决此问题的一般方法的提示:使用接口将您的 API 与您的实现分开。
严格来说只需要对圈子中的一个class做这件事,但对大多数或每个class做这件事可能会提高模块化和编写测试代码的便利性。创建一个接口,MyAppInterface
,在其中声明您的动作侦听器需要直接调用的每个方法(getContentPane()
似乎是您发布的代码中唯一的一个),并让 MyApp
实现它。然后在您的模块配置中将 MyAppInterface
绑定到 MyApp
(在您的情况下,只需更改 providesMyApp
的 return 类型即可),声明动作侦听器提供者采取 MyAppInterface
,和 运行 它。您可能还需要将代码中的其他一些地方从 MyApp
切换到 MyAppInterface
,具体取决于详细信息。
这样做将允许 Guice 本身为您打破循环依赖。当它看到循环依赖时,它将生成一个新的零依赖实现 class of MyAppInterface
作为代理包装器,将其实例传递给动作侦听器提供者,填写依赖链从那里直到它可以创建一个真正的 MyApp
对象,然后它会将 MyApp
对象粘贴到 Guice 生成的对象中,该对象将转发所有方法调用给它。
虽然我在上面使用了 MyAppInterface
,但更常见的命名模式是实际使用接口的 MyApp
名称并将现有的 MyApp
class 重命名为MyAppImpl
.
请注意,Guice 的自动循环依赖中断方法要求您在初始化完成之前不要对生成的代理对象调用任何方法,因为在那之前它不会有包装对象将它们转发到。
编辑:我还没有 Groovy 或 Gradle 准备好尝试 运行 你的 SSCCE 进行测试,但我认为你已经非常接近打破这个圈子了。用 @Singleton
注释 DefaultFizzClient
class 和每个 @Provides
方法,并从 provideExampleApp
中删除 menuBar
参数,我认为应该让它发挥作用。
@Singleton
:class 或提供者方法应标记为 @Singleton
,当只应创建它的单个实例时。当它被注入多个不同的地方或被多次请求时,这一点很重要。使用 @Singleton
,Guice 创建一个实例,保存对它的引用,并每次都使用该实例。如果没有,它会为每个引用创建一个新的单独实例。从概念上讲,这是定义 "how to make an X" 还是 "this here is the X" 的问题。在我看来,您正在创建的 UI 元素属于后一类 - 您的应用程序的单一菜单栏,而不是任何任意菜单栏等。
删除menuBar
:您已经注释掉了该方法中使用它的唯一一行。只需从方法声明中删除参数即可。至于如何让菜单栏进入应用程序,这已经由 addMenuToFrame
方法结合 requestInjection(this)
调用处理。
如果进行这些更改,运行 Guice 将通过的逻辑可能会有所帮助:
- 当您使用
ExampleAppModule
创建注入器时,它会调用模块的configure()
方法。这会设置一些绑定并告诉 Guice,无论何时完成所有绑定设置,它都应该扫描ExampleAppModule
实例以查找用@Inject
注释的字段和方法并填写它们。 configure()
returns 并且,绑定设置完成后,Guice 接受requestInjection(this)
调用。扫描发现addMenuToFrame
被注解为@Inject
。检查该方法,Guice 发现需要一个ExampleApp
实例和一个JMenuBar
实例才能调用它。- Guice 寻找创建
ExampleApp
实例的方法并找到provideExampleApp
方法。 Guice 检查该方法并发现需要一个FizzClient
实例来调用它。 - Guice 寻找创建
FizzClient
实例的方法并找到 class 绑定到DefaultFizzClient
。DefaultFizzClient
有一个默认的无参数构造函数,所以 Guice 只是调用它来获取一个实例。 - 获得
FizzClient
实例后,Guice 现在可以调用provideExampleApp
。它这样做,从而获得一个ExampleApp
实例。 - Guice 仍然需要一个
JMenuBar
才能调用addMenuToFrame
,因此它寻找一种方法来创建一个并找到providesMenuBar
方法。 Guice 检查该方法并指出它需要一个名为 "widgetsMenuActionListener". 的 - Guice 寻找一种方法来创建名为 "widgetsMenuActionListener" 的
ActionListener
并找到providesWidgetsMenuActionListener
方法。 Guice 检查该方法并发现它需要一个ExampleApp
实例和一个名为 "widgetsPanel" 的JPanel
。它已经有一个ExampleApp
实例,在第 5 步中获得,并且它从标记为@Singleton
的东西中获得它,所以它重用相同的实例而不是再次调用provideExampleApp
。 - Guice 寻找一种方法来创建名为 "widgetsPanel" 的
JPanel
并找到providesWidgetPanel
方法。这个方法没有参数,所以Guice只是调用它并获取它需要的JPanel
。 - 除了已经创建的
ExampleApp
之外,还获得了一个具有正确名称的JPanel
,Guice 调用providesWidgetsMenuActionListener
,从而获得一个命名的ActionListener
实例。 - 获得了具有正确名称的
ActionListener
后,Guice 调用providesMenuBar
,从而获得了JMenuBar
实例。 - 终于,终于,获得了一个
ExampleApp
实例和一个JMenuBar
实例,Guice 调用addMenuToFrame
,将菜单添加到框架。 - 现在,在所有这些发生之后,您在
main
中的getInstance(ExampleApp)
调用将被执行。 Guice 检查,发现它已经有一个来自标记为@Singleton
的源的ExampleApp
实例,并且 return 是那个实例。
ActionListener
看完所有这些,似乎 @Singleton
仅在 provideExampleApp
上是绝对必要的,但我认为对其他一切也有意义。