Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Guice cannot instantiate class that extends JPanel - NPE in call to super constructor

We have a desktop Swing application with Google Guice 4.1.0 dependency injection. Everything worked fine during development, but something strange happened when colleague tried to run the application.

We have a MainWindow class that extends JPanel. In the constructor this class takes some controllers that itself are injectable. In main method Guice injector is created. Then the injector tries to instantiate MainWindow (injector.getInstance(MainWindow.class)). And it failed with NullPointerException!

This doesn't happen on my computer, and we use the same JDK.

Here is MainWindow class stripped down to problematic code (note: this does not reproduce the problem, unfortunately):

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

And here is main() method:

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

Here is stack trace of the exception:

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:?]

The NPE was thrown in the most surprising place – in the call to constructor of superclass of MainWindow (this is line 133). I started digging and found out that manual creation of MainWindow and injecting its dependencies works correctly:

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

I suspected that maybe class loader didn't work correctly, so I tried again with logging classloader of both MainWindow and JPanel:

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

Class loaders are different (JPanel is loaded by bootstrap), but now the injection worked properly. I suppose this is because now JPanel class was explicitly loaded into main method context.

So my questions are:

  1. Did anyone have similar problem?
  2. Is it my mistake, or is it a bug?
  3. If it is a bug, does it happen in Guice? Or maybe JRE?

More details about Java and OS:

  • I originally developed it with JDK 1.8.0u111, but then switched to JDK 1.8.0u121.
  • Application is compiled to Java 6.
  • Runs flawlessly on my computer with Windows 10, version 1607 (OS Build 14393.693), on JRE 6 and JRE 8 (from JDK).
  • NullPointerException is raised on colleague's computer with Windows 10, version 1511 (OS Build 10586.753), JDK 1.8.0u112 and 1.8.0u121.

Unfortunately I was unable to provide minimal version that reproduces the problem. Heck, I cannot even reproduce the problem, it happens only on colleague's environment.

like image 644
Archie Avatar asked Feb 28 '17 10:02

Archie


1 Answers

I highly suspect this is due to a race condition. Swing components are not thread safe and should be instantiated on the EDT as per the swing package javadoc :

Swing's Threading Policy

In general Swing is not thread safe. All Swing components and related classes, unless otherwise documented, must be accessed on the event dispatching thread. Typical Swing applications do processing in response to an event generated from a user gesture. For example, clicking on a JButton notifies all ActionListeners added to the JButton. As all events generated from a user gesture are dispatched on the event dispatching thread, most developers are not impacted by the restriction.

Where the impact lies, however, is in constructing and showing a Swing application. Calls to an application's main method, or methods in Applet, are not invoked on the event dispatching thread. As such, care must be taken to transfer control to the event dispatching thread when constructing and showing an application or applet. The preferred way to transfer control and begin working with Swing is to use invokeLater. The invokeLater method schedules a Runnable to be processed on the event dispatching thread.

(emphasis mine)

Now you do start the UI in the EDT, using invokeLater, however you construct the UI on the main thread (through a Guice injector call). The Guice injector call should also be in the invokeLater part to kick off the UI.

like image 114
bowmore Avatar answered Oct 20 '22 01:10

bowmore