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:
More details about Java and OS:
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With