0

Guice 4.0 here. I've been using Guice for a few years now and really love it. I now have a need for a Swing app and am baffled at how to contrive the Guice API to meet Swing's (IMHO) bizarre requirements/best practices.

Typically, when I use Guice, I have a main "driver" class like MyApp which I wire with Guice like so:

// Groovy pseudo-code
class MyApp {
    @Inject
    FizzClient fizzClient

    @Inject
    BuzzClien buzzClient

    static void main(String[] args) {
        MyApp myapp = Guice.createInjector(new MyAppModule()).getInstance(MyApp)
        myapp.run()
    }

    private void run() {
        // Do whatever the app does...
    }
}

class MyAppModule extends AbstractModule {
    @Override
    void configure() {
        bind(FizzClient).to(DefaultFizzClient)
        bind(BuzzClient).to(DefaultBuzzClient)
        // etc...
    }
}

But in the case of Swing, the JFrame is the GUI application, and so to me, it makes sense to have MyApp extend JFrame:

class MyApp extends JFrame {
    // etc...
}

The problem is that, in Swing, you need to do some unconventional thread trickery so that:

  1. Any code that manipulates the JFrame or the other JComponents of the UI needs to happen on the Event Dispatching Thread (EDT) - this is accomplished via SwingUtilities.invokeLater; and
  2. Any code that will block and take a while to finish needs to be siphoned off into its own SwingWorker thread

So let's say I have a need to warm up a large cache at app startup (perhaps this takes 30 - 60 seconds). Here's my best attempt thus far:

@Canonical // Creates a tuple constructor and a bunch of other stuff for me
@Slf4j
class MyApp extends JFrame {
    @Inject
    MyAppCache appCache

    static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            void run() {
                // The Guice-based bootstrapping has to happen inside invokeLater
                // because MyApp *is* a JFrame (hence UI).
                Guice.createInjector(new MyAppModule()).getInsance(MyApp).init()
            }  
        })
    }

    void init() {
        new SwingWorker<Void,String>() {
            @Override
            protected Void doInBackground() {
                appCache.warmUp()
                publish('Cached is warmed up!')
            }

            @Override
            protected void process(List<String> updates) {
                log.info(updates)
            }
        }.execute()
    }
}

class MyAppModule extends AbstractModule {
    @Override
    void configure() {
        bind(MyAppCache).to(DefaultAppCache)
    }

    @Provides
    MyApp providesMyApp(MyAppCache appCache) {
        MyApp app = new MyApp(appCache)
        app.title = 'My App'
        app.defaultCloseOperation = JFrame.EXIT_ON_CLOSE
        app.pack()
        // ...etc. (configure the parent JFrame)
    }
}

When I run this code I get a NPE on the appCache.warmUp() statement inside the SwingWorker. This is because the above code actually involves 3 threads:

  • main - where main & init are running
  • EDT - where Guice is bootstrapping the MyApp instance
  • Swing Worker - where the SwingWorker is running and attempting to warmup the cache

And so by the time we go to execute the Swing worker, Guice hasn't had a chance to bootstrap the MyApp instance yet (and thus, its cache property).

Ay ideas at all (w/ specific code example) how I can get Guice working properly with Swing's concurrency model?

smeeb
  • 27,777
  • 57
  • 250
  • 447
  • 1
    You're invoking `init` yourself in the Runnable passed to `invokeLater`. Why would it be run on the main thread and not the EDT thread? – Jeff Bowman Mar 01 '16 at 17:55
  • 1
    I would be genuinely interested to learn of a GUI library that is _not_ single threaded. – trashgod Mar 01 '16 at 18:05
  • Thanks @JeffBowman (+1) - because `SwingWorker` does some magic - run this code yourself, you'll see the worker code executes inside its own thread, separate from main and the EDT. – smeeb Mar 01 '16 at 18:38
  • I am interested to see the full stacktrace. As you are executing injector.getInstance(MyApp.class).init(), the MyApp is ready, so totally injected. You have a typo in your code ("getInsance") : I assume the code I see here is not the code you execute, and the bug is probably not here. – Jérémie B Mar 01 '16 at 22:57

1 Answers1

2

I'm not familiar with , but you can continue any non-GUI initialization after invoking EventQueue.invokeLater()—such code will simply run on the initial thread. An example may be seen here. Of course, you must synchronize access to any shared data, but you can update the GUI when the initial thread concludes using EventQueue.invokeLater() in the usual way. Your GUI should forestall use of any incomplete data in the interim.

As an aside, I see no reason for MyApp to extend JFrame. Instead, add() your enclosing Container to an instance of JFrame. Several examples are cited here.

Community
  • 1
  • 1
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
  • Thanks @trashgod (+1) - but *believe* me, I've tried everything, even making `MyApp` *not* extend `JFrame`. **However, an answer to the following question might give me a solution here:** So Guice bootstrapping must happen on one of the threads: main, EDT or the Swing Worker ("*Cache Warmer*") thread. Honestly, I don't care which one it is. However, I *need* Guice to have bootstrapped and wired up the `MyApp` instance *before* the Cache Warmer Thread attempts to warm up the cache. – smeeb Mar 01 '16 at 18:33
  • So perhaps all I need to do is to do some synchronization to make sure that the Cache Warmer thread waits for Guice injection to complete. Any ideas how I might accomplish this (even with no Guice experience, all the code is provided above)? – smeeb Mar 01 '16 at 18:33
  • Not sure I can offer a panacea; as suggested [here](http://stackoverflow.com/a/11372932/230513), you can use `CountDownLatch` to synchronize non-GUI threads; also beware of [busy loops](http://stackoverflow.com/q/35154352/230513). – trashgod Mar 02 '16 at 11:26