
Google Guice ist ein leichtgewichtiges Framework für Dependency Injection (DI) in Java und damit eine gute Wahl für ein Java-Programm, das ohne ein Framework wie Spring oder eine Java-EE-Implementierung entwickelt wird.
Begrifflichkeiten und Zusammenhänge
Guice provisioniert Abhängigkeiten in Felder und Parameter, die mit @Inject
annotiert sind. DI wird durch einen Injector realisiert, der von einer übergebenen Klasse eine Instanz erzeugt und dabei alle Abhängigkeiten dieses Objektes und auch seiner Abhängigkeiten (und deren Abhängigkeiten, usw.) auflöst und provisioniert.
Kenntnis über die zu erzeugenden Instanzen erhält der Injector über ein Module. Dabei handelt es sich um eine Sammlung von Bindings, die die konkreten Implementierungen zu einem Interface aufschlüsseln. Hierbei kann auch ein Scope angegeben werden, z.B. Singleton. In diesem Fall wird als Abhängigkeit immer die gleiche „Singleton“-Instanz provisioniert. Es ist auch möglich, im Binding eine bereits erzeugte Instanz zu hinterlegen, anstatt Guice die Instanzierung zu überlassen. Für Objekte, die nicht über den Default Constructor erzeugt werden, können im Binding die Parameter oder ein Factory-Aufruf hinterlegt werden.
Die Annotation @Named
erlaubt es, eine bestimmte Implementierung eines Interfaces zu injizieren. Mit einem Multibinder ist es möglich, mehrere Implementierungen eines Interfaces als Set
zu injizieren.
Injection Types
Der folgende Code zeigt ein einfaches Interface Interface
und eine Klasse Clazz
, die dieses Interface implementiert. Die Klasse Testbed
bekommt eine Instanz von Clazz
injiziert, einmal per Field Injection, einmal per Method Injection und einmal per Constructor Injection. Dafür werden jeweils das Feld fieldInjected
, die Setter-Methode init()
und der Konstruktor mit @Inject
annotiert.
interface Interface {}
class Clazz implements Interface {}
@Getter
class Testbed {
@Inject
private Interface fieldInjected;
private Interface methodInjected;
private Interface constructorInjected;
@Inject
public Testbed(Interface constructorParam) {
this.constructorInjected = constructorParam;
}
@Inject
private void init(Interface methodParam) {
this.methodInjected = methodParam;
}
}
Damit die DI funktioniert, benötigen wir ein Module als Ableitung von com.google.inject.AbstractModule
mit einem Binding, das das Interface Interface
auf die Klasse Clazz
mappt.
Im Test wird für das Module ein neuer Injector erzeugt und von diesem eine Instanz der Klasse Testbed
angefordert. Der Test prüft dann folgende Annahmen ab:
- Für alle drei Felder wurde für die verschiedenen Injection Types eine Instanz der Klasse
Clazz
injiziert. - In allen drei Felder befinden sich verschiedene Instanzen der Klasse
Clazz
.
class Module extends AbstractModule {
@Override
public void configure() {
bind(Interface.class).to(Clazz.class);
}
}
class Test {
@Test
void shouldBeCreated() {
final var testbed = Guice.createInjector(new Module()).getInstance(Testbed.class);
assertThat(testbed).isNotNull();
final var fieldInj = testbed.getFieldInjected();
final var methodInj = testbed.getMethodInjected();
final var constrInj = testbed.getConstructorInjected();
assertThat(fieldInj).isNotNull()
.isInstanceOf(Clazz.class)
.isNotSameAs(methodInj)
.isNotSameAs(constrInj);
assertThat(methodInj).isNotNull()
.isInstanceOf(Clazz.class)
.isNotSameAs(constrInj);
assertThat(constrInj).isNotNull()
.isInstanceOf(Clazz.class);
}
}
Injection Scope
Um eine Abhängigkeit als Singleton zu injizieren, wird das Binding um einen Aufruf von in(Scopes.SINGLETON)
erweitert:
class Module extends AbstractModule {
@Override
public void configure() {
bind(Interface.class).to(Clazz.class).in(Scopes.SINGLETON);
}
}
Ersetzt man das Module
durch diese Variante, schlägt der Test fehl, da sich hinter allen drei Feldern nun die gleiche Variante verbirgt. Um den Test zu korrigieren, ersetzt man den AssertJ-Aufruf isNotSameAs()
durch isSameAs()
.
Für Webapplikationen gibt es neben dem Default Scope, der in jedes Feld eine neue Instanz einschleust, und dem Singleton-Scope auch die Scopes Session und Request. Neben der Deklaration im Binding ist es auch möglich, den Scope an die Implementierung zu hängen, d.h. die Klasse Clazz
mit @Singleton
respektive @SessionScoped
oder @RequestScoped
zu annotieren.
Multibinder
Über Multibinder können alle Implementierungen eines Interfaces in ein Set
geladen werden:
interface Interface {}
class Clazz1 implements Interface {}
class Clazz2 implements Interface {}
class Clazz3 implements Interface {}
@Getter
class Testbed {
@Inject
private Set<Interface> interfaces;
}
Der Multibinder enthält Bindings zwischen einem Interface und verschiedenen Implementierungen. Die Methode binder()
ist Teil des AbstractModules
.
Der Test demonstriert Folgendes:
- Im
Set
sind genau 3 Elemente enthalten. - Bei diesen Elementen handelt es sich jeweils um je eine Instanz von
Clazz1
,Clazz2
undClazz3
.
class Module extends AbstractModule {
@Override
public void configure() {
final var multiBinder = Multibinder.newSetBinder(binder(), Interface.class);
List.of(Clazz1.class, Clazz2.class, Clazz3.class)
.forEach(clazz -> multiBinder.addBinding().to(clazz));
}
}
class Test {
@Test
void shouldBeCreated() {
final var testbed = Guice.createInjector(new Module()).getInstance(Testbed.class);
assertThat(wrapper).isNotNull()
.extracting(Testbed::getInterfaces).isNotNull()
.asList().isNotEmpty()
.hasSize(3)
.hasAtLeastOneElementOfType(Clazz1.class)
.hasAtLeastOneElementOfType(Clazz2.class)
.hasAtLeastOneElementOfType(Clazz3.class);
}
}
Multibinder sind sehr praktisch, wenn man eine Menge an Filtern, Skripten oder Ähnlichem anwenden möchte. Die Konfiguration über DI ermöglicht es, dass beim Hinzufügen oder Entfernen von Filtern etc. lediglich das Module
um die entsprechenden Einträge angepasst werden muss, der restliche Code aber unangetastet bleiben kann.
Annotiertes Binding
Anstelle eines Multibinders können die drei Implementierungen Clazz1
, Clazz2
und Clazz3
auch über explizite Namen geladen werden:
interface Interface {}
class Clazz1 implements Interface {}
class Clazz2 implements Interface {}
class Clazz3 implements Interface {}
@Getter
class Testbed {
@Inject @Named("name1")
private Interface interface1;
@Inject @Named("name2")
private Interface interface2;
@Inject @Named("name3")
private Interface interface3;
}
Damit die Implementierungen über die Annotation @Named
unter dem angegebenen Namen gefunden werden, wird dieser im Binding angegeben:
class Module extends AbstractModule {
@Override
public void configure() {
bind(Interface.class).annotatedWith(Names.named("name1").to(Clazz1.class);
bind(Interface.class).annotatedWith(Names.named("name2").to(Clazz2.class);
bind(Interface.class).annotatedWith(Names.named("name3").to(Clazz3.class);
}
}
Bindung an eine Instanz
Statt Guice die Instanzierung überlassen, kann ein Interface auch an eine konkrete Instanz gebunden werden. Im folgenden Code wird ein Float mit einem Annährerungswert der Zahl Pi als Abhängigkeit gesetzt:
@Getter
class Testbed {
@Inject @Named("Pi")
private float pi;
}
class Module extends AbstractModule {
@Override
public void configure() {
bind(Float.TYPE).annotatedWith(Names.named("Pi")).toInstance(3.141592f);
}
}
Die Annotation ist nicht erforderlich, aber eine gute Idee, da ein Float
etwas sehr Allgemeines und die Zahl Pi etwas sehr Spezielles ist. Durch Bindung an Instanzen lassen sich auf elegante Art und Weise Konfigurationsparameter in die Klassen einschleusen, wo sie benötigt werden, etwa Host und Port für einen Webserver.
Bindung an einen parametrisierten Konstruktor
Die Bindung an Instanzen lässt sich auch nutzen, um Guice einen parametrisierten Konstruktor zur Instanzierung zuzuweisen. Die Annotation @Value
stammt von Lombok. Sie erzeugt automatisch Getter und einen Konstruktor für die deklarierten Felder und wurde hier der Übersichtlichkeit halber verwendet.
interface Interface {}
@Value
class Clazz implements Interface {
String s; int i; boolean b;
}
class Module extends AbstractModule {
@Override
public void configure() {
bind(String.class).toInstance("something");
bind(Integer.class).toInstance(314);
bind(Boolean.class).toInstance(true);
bind(Interface.class).toConstructor(
Clazz.class.getConstructor(String.class, Integer.TYPE, Boolean.TYPE));
}
}
@Getter
class Testbed {
@Inject
private Interface i;
}
Die Mechanik sieht etwas gewöhnungsbedürftig aus. Wir binden String
, int
und boolean
zunächst an drei Instanzen, die wir beim Aufruf des Konstruktors gerne als Argumente mitgeben möchten, und binden dann den Konstruktor an das Interface. Schöner geht das meiner Meinung nach mit einer Factory.
Bindung an eine Factory
Eine Factory kann über eine mit @Provides
annotierte Methode aufgerufen werden. Dabei sind zwei Dinge zu beachten:
- Die Methode muss Teil des
Modules
sein - Die Methode muss in ihrer Signatur als Rückgabetyp das Interface angegeben haben, nicht die Implementierung
Abweichend von den vorherigen Beispielen müssen wir in diesem Fall nicht die Methode configure()
implementieren:
interface Interface {}
@Value
class Clazz implements Interface {
String s; int i; boolean b;
}
class Factory {
public static Interface createClazz() {
return new Clazz("something", 314, true);
}
}
class Module extends AbstractModule {
@Provides
public static Interface provideClazz() {
return Factory.createClazz();
}
}
@Getter
class Testbed {
@Inject
private Interface i;
}
Statt einer mit @Provides
annotierten Methode kann die Factory auch in eine Klasse umgesetzt werden, die com.google.inject.Provider
implementiert. Dies hat insgesamt mehr Code zur Folge, allerdings potenziell weniger Code im Module
selbst, da hier nur das Binding hinterlegt wird:
// Interface, Clazz, Factory und Testbed wie im vorherigen Beispiel
class Module extends AbstractModule {
@Override
public void configure() {
bind(Interface.class).toProvider(FactoryProvider.class);
}
}
class FactoryProvider implements Provider<Interface> {
@Override
public Interface get() {
return Factory.createClazz();
}
}
Guice-Demo
Alle in diesem Beitrag beschriebenen Funktionen von Guice habe ich in einem Demo-Projekt auf GitLab implementiert. Zu allen Beispielen gibt es Unit-Tests, die die korrekte Funktionsweise der DI beweisen. Das Projekt kann mit git clone
ausgecheckt und lokal verwendet werden.
- Guice-Demo auf GitLab