-
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.
Fundamentally, Guice helps you create and retrieve objects for your application to use. These objects that your application needs are called dependencies.
You can think of Guice as being a map[^guice-map]. Your application code declares the dependencies it needs, and Guice fetches them for you from its map. Each entry in the "Guice map" has two parts:
- Guice key: a key in the map which is used to fetch a particular value from the map.
- Provider: a value in the map which is used to create objects for your application.
Guice keys and Providers are explained below.
[^guice-map]: The actual implementation of Guice is far more complicated, but a map is a reasonable approximation for how Guice behaves.
Guice uses Key
to identify a dependency that can be resolved using the
"Guice map".
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> spanishGreetingKey = Key.get(String.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 represent factories in the "Guice map" that are capable of creating objects
to satisfy dependencies.
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 the Provider
interface directly, they
implement a Module
to tell Guice how to configure the injector. Internally,
Guice will create Provider
s for all the objects it knows how to create.
For example, the following Guice module creates two Provider
s:
class DemoModule extends AbstractModule {
@Provides
@Count
static Integer provideCount() {
return 3;
}
@Provides
@Message
static String provideMessage() {
return "hello world";
}
}
-
Provider<String>
that calls theprovideMessage
method and returns "hello world" -
Provider<Integer>
that calls theprovideCount
method and returns3
There are two parts to using Guice:
- Configuration: your application adds things into the "Guice map".
- Injection: your application asks Guice to create and retrieve objects from the map.
Configuration and injection are explained below.
Guice maps are configured using Guice modules (and Just-In-Time bindings). A Guice module is a unit of configuration logic that adds things into the Guice map. There are two ways to do this:
- Adding method annotations like
@Provides
- 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
-->() -> DemoModule.provideMessage()
-
@Count Integer
-->() -> DemoModule.provideCount()
You don't pull things out of a map, you declare that you need them. 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 Guice 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 of its own, 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.
[^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