javaannotationsguicejsr330

Use annotation to feed Google guice MapBinder


In a Java project, build with Gradle 5.2, using Google Guice.

I use the MapBinder (http://google.github.io/guice/api-docs/latest/javadoc/com/google/inject/multibindings/MapBinder.html):

MapBinder<String, Snack> mapbinder
         = MapBinder.newMapBinder(binder(), String.class, Snack.class);
     mapbinder.addBinding("twix").toInstance(new Twix());
     mapbinder.addBinding("snickers").toProvider(SnickersProvider.class);
     mapbinder.addBinding("skittles").to(Skittles.class);

This is working fine, but now, I want a "plugin architecture", so avoid to import all Snack classes, but rather declare it in the class directly, such as:

@SnackImpl("Twix")
class Twix extends Snack {

}

How?


Solution

  • This won't exactly be possible without some expensive classpath scanning: If the injector doesn't have any reference to your Twix class, it would not be able to bind it into a Map without scanning through every JAR on the classpath in search of @SnackImpl-annotated classes. You could try this with Guava's ClassPath, but if you use a network-based or custom classloader, this may not be tractable at all. In any case I wouldn't recommend it.

    One alternative is to use Java's built-in ServiceLoader framework, which lets individual JARs list out the fully-qualified implementations for a given service (interface). You can even use Google's Auto framework to generate that service file for you based on annotations.

    That takes care of listing the implementations, but you'll still need to bind them into the MapBinder. Luckily, MapBinder doesn't require a single definition, and will automatically merge multiple MapBinder definitions during module construction time:

    Contributing mapbindings from different modules is supported. For example, it is okay to have both CandyModule and ChipsModule both create their own MapBinder, and to each contribute bindings to the snacks map. When that map is injected, it will contain entries from both modules.

    (from the MapBinder docs)

    With that in mind, I would recommend that each plugin bundle gets its own Guice module where it registers into a MapBinder, and then you add those Guice modules to the main injector using ServiceLoader to get those modules at injector creation time.

    // Assume CandyPluginModule extends AbstractModule
    
    @AutoService(CandyPluginModule.class)
    public TwixPluginModule extends CandyPluginModule {
      @Override public void configure() {
        MapBinder<String, Snack> mapBinder
           = MapBinder.newMapBinder(binder(), String.class, Snack.class);
        mapBinder.addBinding("twix").to(Twix.class);
      }
    }
    

    You could also take advantage of the superclass:

    @AutoService(CandyPluginModule.class)
    public TwixPluginModule extends CandyPluginModule {
      @Override public void configureSnacks() {  // defined on CandyPluginModule
        bindSnack("twix").to(Twix.class);
      }
    }
    

    Alternatively, you could list implementations like Twix directly with AutoService and then create a Module that reads all of the ServiceLoader implementations into your MapBinder, but that may restrict the flexibility of your plugins and doesn't gain you any decentralization of the bindings that MapBinder doesn't give you already.