-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Multibindings
Overview of Multibinder, MapBinder and OptionalBinder
NOTE: Since Guice 4.2, multibindings support has moved to Guice
core. Before that, you need to depend on the guice-multibindings
extension.
Multibinder and MapBinder are intended for plugin-type architectures, where you've got several modules contributing Servlets, Actions, Filters, Components or even just names.
OptionalBinder is intended to be used by frameworks to:
- define an injection point that may or may not be bound by users.
- supply a default value that can be changed by users.
Using Multibinder
or MapBinder
to host plugins.
Multibindings make it easy to support plugins in your application. Made popular by IDEs and browsers, this pattern exposes APIs for extending the behaviour of an application.
Neither the plugin consumer nor the plugin author need write much setup code for
extensible applications with Guice. Simply define an interface, bind
implementations, and inject sets of implementations! Any module can create a new
Multibinder to contribute bindings to a set of implementations. To illustrate,
we'll use plugins to summarize ugly URIs like http://bit.ly/1mzgW1
into
something readable on Twitter.
First, we define an interface that plugin authors can implement. This is usually an interface that lends itself to several implementations. For this example, we would write a different implementation for each website that we could summarize.
interface UriSummarizer {
/**
* Returns a short summary of the URI, or null if this summarizer doesn't
* know how to summarize the URI.
*/
String summarize(URI uri);
}
Next, we'll get our plugin authors to implement the interface. Here's an implementation that shortens Flickr photo URLs:
class FlickrPhotoSummarizer implements UriSummarizer {
private static final Pattern PHOTO_PATTERN
= Pattern.compile("http://www\\.flickr\\.com/photos/[^/]+/(\\d+)/");
public String summarize(URI uri) {
Matcher matcher = PHOTO_PATTERN.matcher(uri.toString());
if (!matcher.matches()) {
return null;
} else {
String id = matcher.group(1);
Photo photo = lookupPhoto(id);
return photo.getTitle();
}
}
}
The plugin author registers their implementation using a multibinder. Some plugins may bind multiple implementations, or implementations of several extension-point interfaces.
public class FlickrPluginModule extends AbstractModule {
public void configure() {
Multibinder<UriSummarizer> uriBinder = Multibinder.newSetBinder(binder(), UriSummarizer.class);
uriBinder.addBinding().to(FlickrPhotoSummarizer.class);
... // bind plugin dependencies, such as our Flickr API key
}
}
Now we can consume the services exposed by our plugins. In this case, we're summarizing tweets:
public class TweetPrettifier {
private final Set<UriSummarizer> summarizers;
private final EmoticonImagifier emoticonImagifier;
@Inject TweetPrettifier(Set<UriSummarizer> summarizers,
EmoticonImagifier emoticonImagifier) {
this.summarizers = summarizers;
this.emoticonImagifier = emoticonImagifier;
}
public Html prettifyTweet(String tweetMessage) {
... // split out the URIs and call prettifyUri() for each
}
public String prettifyUri(URI uri) {
// loop through the implementations, looking for one that supports this URI
for (UrlSummarizer summarizer : summarizers) {
String summary = summarizer.summarize(uri);
if (summary != null) {
return summary;
}
}
// no summarizer found, just return the URI itself
return uri.toString();
}
}
Note: The method Multibinder.newSetBinder(binder, type)
can be confusing.
This operation creates a new binder, but doesn't override any existing bindings.
A binder created this way contributes to the existing Set of implementations for
that type. It would create a new set only if one is not already bound.
Finally we must register the plugins themselves. The simplest mechanism to do so is to list them programatically:
public class PrettyTweets {
public static void main(String[] args) {
Injector injector = Guice.createInjector(
new GoogleMapsPluginModule(),
new BitlyPluginModule(),
new FlickrPluginModule()
...
);
injector.getInstance(Frontend.class).start();
}
}
If it is infeasible to recompile each time the plugin set changes, the list of plugin modules can be loaded from a configuration file.
Note that this mechanism cannot load or unload plugins while the system is running. If you need to hot-swap application components, investigate Guice's OSGi.
By default Multibinder
does not allow duplicates and a DUPLICATE_ELEMENT
error will be thrown when duplicate elements are added to the Multibinder
.
Note that Guice itself deduplicates if you bind the same constant value twice,
this error is only thrown if a duplicate is encountered during provisioning,
e.g. when two providers return the same value. To allow duplicates, you can use
permitDuplicates
API on Multibinder
:
public class FlickrPluginModule extends AbstractModule {
@Override
protected void configure() {
Multibinder<UriSummarizer> uriBinder =
Multibinder.newSetBinder(binder(), UriSummarizer.class);
uriBinder.permitDuplicates();
}
}
The previous example shows how to bind a Set<UrlSummarizer>
with multiple
modules using Multibinder
. Guice also supports binding Map<K, V>
using
MapBinder
(e.g a Map<String, UrlSummarizer
>):
public class FlickrPluginModule extends AbstractModule {
public void configure() {
MapBinder<String, UriSummarizer> uriBinder =
MapBinder.newMapBinder(binder(), String.class, UriSummarizer.class);
uriBinder.addBinding("Flickr").to(FlickrPhotoSummarizer.class);
... // bind plugin dependencies, such as our Flickr API key
}
}
Applications then can inject Map<String, UriSummarizer>
like:
public class TweetPrettifier {
private final Map<String, UriSummarizer> summarizers;
@Inject TweetPrettifier(Map<String, UriSummarizer> summarizers) {
this.summarizers = summarizers;
...
}
}
Like Multibinder
, MapBinder
by default does not allow duplicates and a
DUPLICATE_ELEMENT
error will be thrown when duplicate elements are added to
the MapBinder
. However, unlike Multibinder
, the uniqueness requirement is on
the key of the entry and not the value.
To allow duplicates, you can use permitDuplicates
API on MapBinder
. When
there are duplicate keys, the actual value that ends up in the final Map
is
unspecified:
public final class FooModule extends AbstractModule {
@Override
protected void configure() {
MapBinder.newMapBinder(binder(), String.class, String.class)
.permitDuplicates();
}
}
public final class BarModule extends AbstractModule {
@ProvidesIntoMap
@StringMapKey("letter")
String provideKeyValue() {
return "a";
}
}
public final class BazModule extends AbstractModule {
@ProvidesIntoMap
@StringMapKey("letter")
String provideKeyValue() {
return "b";
}
}
In the above example, the key letter
in the final Map<String, String>
may
have value a
or b
.
OptionalBinder
can be used to provide optional bindings.
Frameworks often expose configuration APIs for application developers to
customize the framework's behavior. OptionalBinder
can make requiring optional
binding easy when Guice bindings are used to customize this type of
configurations.
For example, a web framework might have an API for application to supply an
optional RequestLogger
to log request and response:
public class FrameworkLoggingModule extends AbstractModule {
protected void configure() {
OptionalBinder.newOptionalBinder(binder(), RequestLogger.class);
}
}
With this module, an Optional<RequestLogger>
can be injected by the framework
code to log the request and response after processing a request:
public class RequestHandler {
private final Optional<RequestLogger> requestLogger;
@Inject
RequestHandler(Optional<RequestLogger> requestLogger) {
this.requestLogger = requestLogger;
}
void handleRequest(Request request) {
Response response = ...;
requestLogger.ifPresent(
logger -> logger.logRequest(request, response));
}
}
When the application doesn't provide a RequestLogger
, no logging is done. If
the application installs a module like:
public class ConsoleLoggingModule extends AbstractModule {
@Provides
RequestLogger provideRequestLogger() {
return new ConsoleLogger(System.out);
}
}
The framework code will get a present value that contains an instance of
ConsoleLogger
to log the request and response.
In the above example, a RequestLogger
is optional to the framework but that is
not always the case. When a binding is required, the framework can use
OptionalBinder
to set a default binding that can be overidden by the
application:
public class FrameworkLoggingModule extends AbstractModule {
protected void configure() {
OptionalBinder.newOptionalBinder(binder(), RequestLogger.class)
.setDefault()
.to(DefaultRequestLoggerImpl.class);
}
}
With the above module, the framework's default DefaultRequestLoggerImpl
is
used when no application binding for RequestLogger
is supplied.
If one module uses setDefault
the only way to override the default is to use
setBinding
. It is an error for a user to specify the binding without using
OptionalBinder
if setDefault
or setBinding
are called.
So to override the framework's default logger binding, application can install a module like:
public class ConsoleLoggingModule extends AbstractModule {
@Override
protected void configure() {
OptionalBinder.newOptionalBinder(binder(), RequestLogger.class)
.setBinding()
.to(ConsoleLogger.class);
}
}
Besides using the Multibinder
and MapBinder
to create multibindings in a
module's configure
method, you can also use @Provides-like methods to add
bindings to a Multibinder
, MapBinder
or OptionalBinder
.
public class FlickrPluginModule extends AbstractModule {
@ProvidesIntoSet
UriSummarizer provideFlickerUriSummarizer() {
return new FlickrPhotoSummarizer(...);
}
}
public class FlickrPluginModule extends AbstractModule {
@StringMapKey("Flickr")
@ProvidesIntoMap
UriSummarizer provideFlickrUriSummarizer() {
return new FlickrPhotoSummarizer(...);
}
}
@ProvidesIntoMap
requires an extra annotation to specify the key associated
with the binding. The above example uses @StringMapKey
annotation, which is
one of the built-in annotations that can be used with @ProvidesIntoMap
, to
associate the binding provided by provideFlickerUriSummarizer
with the key
"Flickr"
.
You can create custom annotation that can be used with @ProvidesIntoMap
by
annotating an annotation with MapKey
annotation like:
@MapKey(unwrapValue=true)
@Retention(RUNTIME)
public @interface MyCustomEnumKey {
MyCustomEnum value();
}
If unwrapValue = true
, then the value of the custom annotation is used as the
key of the map, otherwise the whole annotation is used as the key. The above
example of MyCustomEnumKey
has unwrapValue = true
, so the corresponding
MapBinder
uses MyCustomEnum
as the key instead of MyCustomEnumKey
itself.
Framework code can use
@ProvidesIntoOptional(ProvidesIntoOptional.Type.DEFAULT)
to provide a default
binding and application code can use
@ProvidesIntoOptional(ProvidesIntoOptional.Type.ACTUAL)
to override the
default binding.
public class FrameworkModule extends AbstractModule {
@ProvidesIntoOptional(ProvidesIntoOptional.Type.DEFAULT)
@Singleton
RequestLogger provideConsoleLogger() {
return new DefaultRequestLoggerImpl();
}
}
public class RequestLoggingModule extends AbstractModule {
@ProvidesIntoOptional(ProvidesIntoOptional.Type.ACTUAL)
@Singleton
RequestLogger provideConsoleLogger() {
return new ConsoleLogger(System.out);
}
}
NOTE: Currently @ProvidesIntoOptional
can't be used to create an
absent/empty optional binding and OptionalBinder.newOptionalBinder
must be
used instead.
When you use PrivateModule
s with multibindings, all of the elements must be
bound in the same environment. You cannot create collections whose elements span
private modules. Otherwise injector creation will fail.
(new in Guice 3.0)
Sometimes you need to inspect the elements that make up a Multibinder or MapBinder. For example, you may need a test that strips all elements of a MapBinder out of a series of modules. You can visit a binding with a MultibindingsTargetVisitor to get details about Multibindings or MapBindings. After you have an instance of a MapBinderBinding or a MultibinderBinding you can learn more.
// Find the MapBinderBinding and use it to remove elements within it.
Module stripMapBindings(Key<?> mapKey, Module... modules) {
MapBinderBinding<?> mapBinder = findMapBinder(mapKey, modules);
List<Element> allElements = Lists.newArrayList(Elements.getElements(modules));
if (mapBinder != null) {
List<Element> mapElements = getMapElements(mapBinder, modules);
allElements.removeAll(mapElements);
}
return Elements.getModule(allElements);
}
// Look through all Elements in the module and, if the key matches,
// then use our custom MultibindingsTargetVisitor to get the MapBinderBinding
// for the matching binding.
MapBinderBinding<?> findMapBinder(Key<?> mapKey, Module... modules) {
for(Element element : Elements.getElements(modules)) {
MapBinderBinding<?> binding =
element.acceptVisitor(new DefaultElementVisitor<MapBinderBinding<?>>() {
MapBinderBinding<?> visit(Binding<?> binding) {
if(binding.getKey().equals(mapKey)) {
return binding.acceptTargetVisitor(new Visitor());
}
return null;
}
});
if (binding != null) {
return binding;
}
}
return null;
}
// Get all elements in the module that are within the MapBinderBinding.
List<Element> getMapElements(MapBinderBinding<?> binding, Module... modules) {
List<Element> elements = Lists.newArrayList();
for(Element element : Elements.getElements(modules)) {
if(binding.containsElement(element)) {
elements.add(element);
}
}
return elements;
}
// A visitor that just returns the MapBinderBinding for the binding.
class Visitor
extends DefaultBindingTargetVisitor<Object, MapBinderBinding<?>>
implements MultibindingsTargetVisitor<Object, MapBinderBinding<?>> {
MapBinderBinding<?> visit(MapBinderBinding<?> mapBinder) {
return mapBinder;
}
MapBinderBinding<?> visit(MultibinderBinding<?> multibinder) {
return null;
}
}
-
User's Guide
-
Integration
-
Extensions
-
Internals
-
Releases
-
Community