Guice无法实例化扩展JPanel的类 - 在调用超级构造函数时出现NPE。

7
我们有一个桌面Swing应用程序,依赖于Google Guice 4.1.0进行依赖注入。在开发期间一切正常,但当同事尝试运行该应用程序时出现了奇怪的问题。
我们有一个MainWindow类扩展了JPanel。在构造函数中,此类使用可注入的控制器。在主方法中创建Guice注入器。然后注入器尝试实例化MainWindow(injector.getInstance(MainWindow.class))。结果失败,并抛出NullPointerException
这在我的电脑上没有发生过,而且我们使用相同的JDK。
以下是MainWindow类被削减到有问题的代码(注意:不幸的是,这不能重现问题):
class MainWindow extends JPanel {
    private final Foo foo;

    private final JFrame frame;

    @Inject
    public MainWindow(Foo foo) {
        super(new GridBagLayout()); // <-- NullPointerException
        this.foo = foo;
        this.frame = new JFrame("title");
    }

    public void createAndShowGUI() {
        // ...
        frame.add(this);
        frame.pack();
        frame.setVisible(true);
    }
}

这里是 main() 方法:

class Main {
    private static final Injector injector = Guice.createInjector();

    public static void main(String[] args) {
        MainWindow mainWindow = injector.getInstance(MainWindow.class);

        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                mainWindow.createAndShowGUI();
            }
        });
    }
}

以下是异常的堆栈跟踪信息:

com.google.inject.ProvisionException: Unable to provision, see the following errors:

1) Error injecting constructor, java.lang.NullPointerException
  at app.gui.MainWindow.<init>(MainWindow.java:133)
  while locating app.gui.MainWindow

1 error
        at com.google.inject.internal.InjectorImpl$2.get(InjectorImpl.java:1028) ~[app-1.0-SNAPSHOT.jar:?]
        at com.google.inject.internal.InjectorImpl.getInstance(InjectorImpl.java:1054) ~[app-1.0-SNAPSHOT.jar:?]
        at app.Main.createAndShowGUI(Main.java:40) ~[app-1.0-SNAPSHOT.jar:?]
        at app.Main.access$000(Main.java:26) ~[app-1.0-SNAPSHOT.jar:?]
        at app.Main$2.run(Main.java:67) ~[app-1.0-SNAPSHOT.jar:?]

空指针异常在最出人意料的地方抛出 - 在MainWindow的超类构造函数调用中(这是第133行)。我开始深入研究,发现手动创建MainWindow并注入其依赖项是正确的:

MainWindow mainWindow = new MainWindow(injector.getInstance(Foo.class));

我怀疑可能是类加载器没有正确工作,因此我尝试使用MainWindowJPanel的日志类加载器再次尝试:

System.out.println("MainWindow: " + MainWindow.class.getClassLoader());
System.out.println("JPanel:     " + JPanel.class.getClassLoader());
MainWindow mainWindow = injector.getInstance(MainWindow.class);

类加载器是不同的(JPanel由引导程序加载),但现在注入已经正常工作了。我想这是因为现在JPanel类被显式地加载到主方法上下文中。

所以我的问题是:

  1. 有人遇到过类似的问题吗?
  2. 是我的错误,还是一个bug?
  3. 如果这是一个bug,它会发生在Guice中吗?或者是JRE?

关于Java和操作系统的更多细节:

  • 我最初使用JDK 1.8.0u111开发,但后来切换到JDK 1.8.0u121。
  • 应用程序编译为Java 6。
  • 在我的计算机上(Windows 10,版本1607(OS Build 14393.693),JRE 6和JRE 8(来自JDK)),运行非常顺畅。
  • 在同事的计算机上(Windows 10,版本1511(OS Build 10586.753),JDK 1.8.0u112和1.8.0u121),会出现NullPointerException

不幸的是,我无法提供能够重现问题的最小版本。该问题只在同事的环境中发生。


1
已更新问题并附上堆栈跟踪。 - Archie
2
@Nektie 我对“当您必须在实现中实例化一个对象时,它就会破坏DI的目的”毫不认同。有很多对象 - 数据对象、UI元素等 - 它们不需要DI提供的依赖项,也没有合理的替代实现(包括模拟)可用。 - Jeff Bowman
@Archie,你在MainWindow中是否重写了setLayoutsetDoubleBufferedsetUIPropertyupdateUIsetLocaleenableEvents或其他系统调用?JPanel 在其构造函数中调用了一些可重写方法,这意味着子类可以读取尚未设置的非空终态字段。(另外,我很惊讶你可以在没有父级的JPanel上调用show,而不是JFrame或其他明确的顶级组件。) - Jeff Bowman
我非常尊重您的意见,但是如果您正在使用 DI 框架,那么您应该有一个单一的依赖项交付点,无论您是否能够提供模拟或替代实现。这使得代码更易于跟踪和更加模块化。 - Nektie
1
我曾经遇到过同样的问题,但是使用的是Swing(没有GUICE)和Spring Boot。我通过以下方式解决了这个问题:1.在EDT可运行程序之前调用Toolkit.getDefaultToolkit(); System.setProperty("java.awt.headless", "true");,而不是你声明MainWindow的位置。2.将注入移动到EDT可运行程序中,并调用,在你的情况下:injector.getInstance(MainWindow.class).createAndShowGUI();。虽然不确定是否有帮助,但你可以尝试一下。 - rdlopes
显示剩余9条评论
1个回答

1
我高度怀疑这是由于竞态条件引起的。Swing组件不是线程安全的,应该根据swing包javadoc在EDT上实例化:

Swing的线程策略

总的来说,Swing不是线程安全的。所有Swing组件和相关类(除非另有说明)必须在事件分派线程上访问。典型的Swing应用程序会响应用户手势生成的事件进行处理。例如,单击JButton会通知添加到JButton的所有ActionListeners。由于所有从用户手势生成的事件都在事件分派线程上调度,因此大多数开发人员不受限制。

然而,影响的地方在于构建和显示Swing应用程序。对应用程序的主方法或Applet中的方法的调用不会在事件分派线程上调用。因此,在构建和显示应用程序或applet时必须注意将控制权转移到事件分派线程。传递控制并开始使用Swing的首选方法是使用invokeLater。invokeLater方法安排一个Runnable在事件分派线程上处理。

(强调我的)

现在你需要在EDT中使用invokeLater启动UI,但是你需要通过主线程(通过Guice注入器调用)构建UI。 Guice注入器调用也应该在invokeLater部分,以启动UI。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接