-
Notifications
You must be signed in to change notification settings - Fork 1.7k
MentalModel
Learn about Key
, Provider
and how Guice is just a map
When you are reading about Guice, you often see many buzzwords ("Inversion of control", "Hollywood principle", "injection") that make it sound confusing. But underneath the jargon of dependency injection, the concepts aren't very complicated. In fact, you might have written something very similar already! This page walks through a simplified model of Guice's implementation, which should make it easier to think about how it works.
Note: While this page doesn't assume any prior knowledge of Guice or dependency injection, it does assume a prior working knowledge of Java, including modern Java syntax with annotations, method references, and lambdas, as well as knowledge of common object-oriented programming principles and patterns.
Guice uses Key
to identify a dependency that can be resolved.
The Greeter
class used in the
Getting Started Guice declares two
dependencies in its constructor and those dependencies are represented as Key
in Guice:
-
@Message String
-->Key<String>
-
@Count int
-->Key<Integer>
The simplest form of a Key
represents a type in Java:
// Identifies a dependency that is an instance of String.
Key<String> databaseKey = Key.get(String.class);
However, applications often have dependencies that are of the same type:
final class MultilingualGreeter {
private String englishGreeting;
private String spanishGreeting;
MultilingualGreeter(String englishGreeting, String spanishGreeting) {
this.englishGreeting = englishGreeting;
this.spanishGreeting = spanishGreeting;
}
}
Guice uses binding annotations to distinguish dependencies that are of the same type, that is to make the type more specific:
final class MultilingualGreeter {
private String englishGreeting;
private String spanishGreeting;
@Inject
MultilingualGreeter(
@English String englishGreeting, @Spanish String spanishGreeting) {
this.englishGreeting = englishGreeting;
this.spanishGreeting = spanishGreeting;
}
}
Key
with binding annotations can be created as:
Key<String> englishGreetingKey = Key.get(String.class, English.class);
Key<String> spannishGreetingKey = Key.get(Database.class, Spanish.class);
When an application calls injector.getInstance(MultilingualGreeter.class)
to
create an instance of MultilingualGreeter
. This is the equivalent of doing:
// Guice internally does this for you so you don't have to wire up those
// dependencies manually.
String english = injector.getInstance(Key.get(String.class, English.class));
String spanish = injector.getInstance(Key.get(String.class, Spanish.class));
MultilingualGreeter greeter = new MultilingualGreeter(english, spanish);
To summarize: Guice Key
is a type combined with an optional binding
annotation used to identify dependencies.
Guice uses
Provider
to represents factories that are capable of creating objects to satisfy
dependency requirements.
Provider
is an interface with a single method:
interface Provider<T> {
/** Provides an instance of T.**/
T get();
}
Each class that implements Provider
is a bit of code that knows how to give
you an instance of T
. It could call new T()
, it could construct T
in some
other way, or it could return you a precomputed instance from a cache.
Most applications do not implement Provider
interface directly, they use
Module
to configure Guice injector and Guice injector internally creates
Provider
s for all the object it knows how to create.
For example, the following Guice module creates two Provider
s:
class DemoModule extends AbstractModule {
protected void configure() {
bind(Key.get(String.class, Message.class)).toInstance("hello world");
}
@Provides
@Count
static Integer provideCount() {
return 3;
}
}
-
Provider<String>
that returns the message "hello world" -
Provider<Integer>
that calls theprovideCount
method and returns3
A good way to think about Guice is to imagine it as a big map of Map<Key<?>, Provider<?>>
(Of course, that's not type safe because the two wildcard
generics won't match. This is a simplified mental model, not a rigorous
implementation.)
Like Provider
s, most applications do not create Key
directly but rather use
Module
s to configure the mapping from Key
to Provider
.
Modules exist as bundles of configuration logic that just add things into the
Guice map. There are two ways to do this: either using method annotations like
@Provides
, or by using the Guice Domain Specific Language (DSL).
Conceptually, these APIs simply provide ways to manipulate the Guice map. The manipulations they do are pretty straightforward. Here are some example translations, shown using Java 8 syntax for brevity and clarity:
Guice DSL syntax | Mental model |
---|---|
bind(key).toInstance(value) |
map.put(key, () -> value) |
: : (instance binding) : |
|
bind(key).toProvider(provider) |
map.put(key, provider) (provider |
: : binding) : | |
bind(key).to(anotherKey) |
map.put(key, map.get(anotherKey)) |
: : (linked binding) : |
|
@Provides Foo provideFoo() {...} |
`map.put(Key.get(Foo.class), |
: : module::provideFoo)` (provider : |
|
: : method binding) : |
DemoModule
adds two entries into the Guice map:
-
@Message String
-->() -> "hello world"
-
@Count Integer
-->() -> DemoModule.provideCount()
This is the essence of dependency injection. If you need something, you don't go out and get it from somewhere, or even ask a class to return you something. Instead, you simply declare that you can't do your work without it, and rely on someone else to give you what you need.
This model is backwards from how most people think about code: it's a more declarative model rather than an imperative one. This is why dependency injection is often described as a kind of inversion of control (IoC).
Some ways of declaring that you need something:
-
An argument to an
@Inject
constructor:class Foo { private Database database; @Inject Foo(Database database) { // We need a database, from somewhere this.database = database; } }
-
An argument to a
@Provides
method:@Provides Database provideDatabase( // We need the @DatabasePath String before we can construct a Database @DatabasePath String databasePath) { return new Database(databasePath); }
This example is intentionally the same as the example Foo
class from
Getting Started Guide, adding
only the @Inject
annotation on the constructor, which marks the constructor as
being available for Guice to use.
When injecting a thing that has dependencies, Guice recursively injects the
dependencies. You can imagine that in order to inject an instance of Foo
as
shown above, Guice creates Provider
implementations that look like these:
class FooProvider implements Provider<Foo> {
@Override
public Foo get() {
Provider<Database> databaseProvider = guiceMap.get(Key.get(Database.class));
Database database = databaseProvider.get();
return new Foo(database);
}
}
class ProvideDatabaseProvider implements Provider<Database> {
@Override
public Database get() {
Provider<String> databasePathProvider =
guiceMap.get(Key.get(String.class, DatabasePath.class));
String databasePath = databasePathProvider.get();
return module.provideDatabase(databasePath);
}
}
Dependencies form a directed graph, and injection works by doing a depth-first traversal of the graph from the object you want up through all its dependencies.
A Guice Injector
object represents the entire dependency graph. To create an
Injector
, Guice needs to validate that the entire graph works. There can't be
any "dangling" nodes where a dependency is needed but not provided.[^3] If the
graph is invalid for any reason, Guice throws a CreationException
that
describes what went wrong.
Usually, a CreationException
just means that you're trying to use something
(like a Database
), but you forgot to include the module that provides it.
Other times, it can mean that you mistyped something.
[^3]: The reverse case is not an error: it's fine to provide something even if nothing ever uses it—it's just dead code in that case. That said, just like any dead code, it's best to delete providers if nobody uses them anymore.
Learn how to use Scopes
to manage the lifecycle of objects created
by Guice and the many different ways to
add entries into the Guice map.
-
User's Guide
-
Integration
-
Extensions
-
Internals
-
Releases
-
Community