Java 避免Guice中框架强加的循环依赖

Java 避免Guice中框架强加的循环依赖,java,swing,guice,Java,Swing,Guice,请注意:尽管这个问题特别提到了Swing,但我认为这是一个纯粹的Guice(4.0)问题,它突出了Guice和其他固执己见的框架可能存在的一般性问题 在Swing中,您有应用程序UI,它扩展了JFrame: // Pseudo-code class MyApp extends JFrame { // ... } class MyModule extends AbstractModule { @Override void configure() { requestInjec

请注意:尽管这个问题特别提到了Swing,但我认为这是一个纯粹的Guice(4.0)问题,它突出了Guice和其他固执己见的框架可能存在的一般性问题


在Swing中,您有应用程序UI,它扩展了
JFrame

// Pseudo-code
class MyApp extends JFrame {
    // ...
}
class MyModule extends AbstractModule {
  @Override void configure() {
    requestInjection(this);
  }

  @Inject void addMenuToFrame(JFrame frame, JMenuBar menu) {
    frame.setMenuBar(menu);
  }
}
你的应用程序(
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
您的菜单项需要动作监听器,其中许多监听器将更新您应用程序的
内容窗格

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
    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上的项目,了解SSCE。

    好吧,如果你有一个循环依赖,它就不可能是一个创造性的循环依赖,因为否则你根本无法创建对象的循环

    您可以通过在模块上请求注入来执行创建后配置。例如,您可以使用它将
    JMenuBar
    添加到
    JFrame

    // Pseudo-code
    class MyApp extends JFrame {
        // ...
    }
    
    class MyModule extends AbstractModule {
      @Override void configure() {
        requestInjection(this);
      }
    
      @Inject void addMenuToFrame(JFrame frame, JMenuBar menu) {
        frame.setMenuBar(menu);
      }
    }
    
    我还没有测试过它是否有效(因为您没有提供足够的代码来尝试它)——因此您可能需要在正确的位置进行试验,以打破当前的循环,以便随后像这样加入它——但类似的东西应该适合您


    我已经回答了一些关于注入guice模块的其他问题,比如-,因为其他人也有一些关于这方面的好提示,而不是我试图在这里正确地重复它们



    为了帮助我理解,我一直在进一步研究这个问题,我发现需要将
    JFrame
    JMenuBar
    绑定到单例范围内,这样才能有效地工作,因为方法注入是在创建注入器时发生的,在随后创建
    JFrame
    JMenuBar
    实例时不会这样做(可能很明显)。

    我回答这个问题时,无法根据您的代码测试答案,因为我不知道(也不想学)Groovy

    但基本上,当您有循环依赖项时,打破它的最佳方法是使用
    提供程序

    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();
      }
    }
    
    包so36042838;
    导入com.google.common.base.*;
    导入com.google.inject.Guice;
    导入javax.inject.*;
    公开课问题{
    @单例公共最终静态类A{
    B B;
    @注入A(B B){this.B=B;}
    void use(){System.out.println(“正在进行”);}
    }
    @单例公共最终静态类B{
    供应商a;
    @注入B(提供者a){this.a=Suppliers.memoize(a::get);}//按预期打印“通过”
    void makeUseOfA(){a.get().use();}
    }
    公共静态void main(字符串[]args){
    Guice.createInjector().getInstance(B.class.makeUseOfA();
    }
    }
    
    有几种技术可用于重构循环依赖项,使其不再是循环依赖项,以及。对于不可能的情况,错误消息Guice提供了Guice解决此问题的一般方法提示:使用接口将API与实现分离

    严格地说,只需要对循环中的一个类执行此操作,但对大多数或每个类执行此操作可能会提高模块性和编写测试代码的方便性。创建一个接口,
    MyAppInterface
    ,在其中声明操作侦听器需要直接调用的每个方法(
    getContentPane()
    似乎是您发布的代码中唯一的方法),并让
    MyApp
    实现它。然后在模块配置中将
    MyAppInterface
    绑定到
    MyApp
    (在您的情况下,只需更改
    providesMyApp
    的返回类型即可),声明操作侦听器提供程序获取
    MyAppInterface
    ,然后运行它。您可能还需要将代码中的一些其他位置从
    MyApp
    切换到
    MyAppInterface
    ,具体取决于详细信息

    这样做将允许Guice本身为您打破循环依赖关系。当它看到循环依赖时,它将生成一个新的零依赖实现类
    MyAppInterface
    ,该类充当代理包装器,将该类的一个实例传递到动作侦听器提供程序,从那里填充依赖链,直到它可以生成一个真正的
    MyApp
    对象,然后它会将
    MyApp
    对象粘贴在Guice生成的对象中,该对象会将所有方法调用转发给它

    尽管我在上面使用了
    MyAppInterface
    ,但更常见的命名模式是实际使用接口的
    MyApp
    名称,并将现有的
    MyApp
    类重命名为
    MyAppImpl

    请注意,Guice的自动循环依赖打破方法要求在初始化完成之前不要调用生成的代理对象上的任何方法,因为在初始化完成之前,它不会有一个包装对象将它们转发给它

    编辑:我还没有Groovy或Gradle准备好尝试运行SSCCE进行测试,但我认为您已经非常接近打破这个循环了。注释