-
-
Notifications
You must be signed in to change notification settings - Fork 0
NeuronDIAtGuiceForJavaTutorial
This tutorial explains how to use Neuron DI @ Guice For Java. It is assumed that you are already familiar with Guice. If not, please consult the Guice Wiki first or consider using Neuron DI For Java standalone.
Neuron DI @ Guice provides an API which extends Guice with lazy dependency resolution and caching. It also extends the domain specific language (DSL) for Guice binding definitions to ease some pain.
Note that Neuron DI does not at all compete with Guice: As you will see, Neuron DI is complementary to JSR-330. Guice is an implementation of JSR-330, so combining forces makes perfect sense if you are using Guice anyway. If you are not using Guice however, Neuron DI @ Guice adds nothing which could not be done with standalone Neuron DI.
Let's start with pure Guice and then see how Neuron DI @ Guice can improve things.
Consider the following contrived example for a Greeting
interface:
interface Greeting {
/** Returns a greeting message for the given entity. */
String message(String entity);
}
The Greeting
interface is implemented by GuiceGreeting
, which depends on the Formatter
interface:
class GuiceGreeting implements Greeting {
@Inject
private Formatter formatter;
@Override
public String message(String entity) { return formatter.format(entity); }
}
interface Formatter {
/** Returns a text which has been formatted using the given arguments. */
String format(Object... args);
}
For brevity, private field injection is used in the GuiceGreeting
class although it's considered bad practice because
now you must use a DI framework to create instances of this class, which is bad for testing!
We are going to refactor this class later anyway, so let's accept this code smell for now.
Last but not least, the Formatter
interface is implemented by the GuiceFormatter
class:
class GuiceFormatter implements Formatter {
@Inject
@Named("format")
private String format;
@Override
public String format(Object... args) { return String.format(format, (Object[]) args); }
}
Again, private field injection is used.
In order to disambiguate the format string from other strings when configuring the bindings later, we need to add a
qualifier annotation.
In this case, its @Named("format")
.
Now we can define a module to bind the components:
class GuiceGreetingModule extends AbstractModule {
@Override
protected void configure() {
bind(Greeting.class).to(GuiceGreeting.class).in(Singleton.class);
bind(Formatter.class).to(GuiceFormatter.class);
bindConstant().annotatedWith(Names.named("format")).to("Hello %s!");
}
}
Note that the qualifying annotation is referenced by Names.named("format")
to specifically bind the format string to
"Hello %s!"
.
Note also that Greeting
is configured to be a singleton, which is required to pass the following test:
public class GuiceGreetingModuleTest extends GreetingModuleTest {
GuiceGreetingModuleTest() { super(new GuiceGreetingModule()); }
}
public abstract class GreetingModuleTest {
private final Injector injector;
protected GreetingModuleTest(Module module) { injector = Guice.createInjector(module); }
private <T> T getInstance(Class<T> type) { return injector.getInstance(type); }
@Test
public void testGreeting() {
final Greeting greeting = getInstance(Greeting.class);
assertThat(greeting.message("world"), is("Hello world!"));
assertThat(getInstance(Greeting.class), is(sameInstance(greeting)));
}
@Test
public void testFormatter() {
final Formatter formatter = getInstance(Formatter.class);
assertThat(formatter.format("world"), is("Hello world!"));
assertThat(getInstance(Formatter.class), is(not(sameInstance(formatter))));
}
}
Note that this is actually an integration test which tests GuiceGreetingModule
with its configured implementations of
the Greeting
and Formatter
interfaces.
In practice, you might want to write a unit test for GuiceGreeting
and GuiceFormatter
instead and skip testing the
GuiceGreetingModule
.
Note also that the two test cases are almost identical, except for the scopes.
This is because GuiceGreeting.greeting(String)
just forwards its parameter to Formatter.format(Object[])
.
In practice, this should be different or otherwise you wouldn't need the whole class.
Now let's see how we can extend Guice with Neuron DI. Please read the project setup page first.
Next, import the Neuron DI @ Guice API:
import global.namespace.neuron.di.guice.java.*;
Thanks to interface segregation in the design of this example, the Greeting
and Formatter
interfaces don't have to
change, only their implementations do.
So here's the implementation of the Greeting
interface using Neuron DI:
@Neuron
interface NeuronGreeting extends Greeting {
@Caching
Formatter getFormatter();
default String message(String entity) { return getFormatter().format(entity); }
}
The most notable difference to the GuiceGreeting
class is that NeuronGreeting
is actually an interface:
This improves its reusability because now you can mix-in NeuronGreeting
to (non-final) classes.
Note that this is not a requirement though: NeuronGreeting
could just be an (abstract) class as well.
In comparison, injection into interfaces is not possible with Guice (or any other JSR-330 implementation) because in
Java 8, interfaces can have no fields, hence no state and hence constructor and method injection are not applicable,
either.
Let's have a closer look at the two annotations:
A class or interface annotated with @Neuron
is called a "neuron class" or "neuron interface".
The annotation signals that the neuron class or interface is subject to dependency injection.
Within a neuron class or interface, any abstract, parameterless method is called a "synapse method".
No special annotation is required for synapse methods, however, so getFormatter()
would be a synapse method even if it
did not have the @Caching
annotation.
Synapse methods are the injection points in Neuron DI, so Neuron DI will resolve and return an instance of the return
type when the method is called.
As an implication of this design, all dependency resolution is inherently lazy.
A method annotated with @Caching
can only exist in a neuron class or interface.
The annotation signals that the method is subject to a caching strategy.
If there is no parameter, the applied caching strategy is thread-safe (which is equivalent to a lazy val
declaration
in Scala).
Note that the annotation is not only applicable to synapse methods:
In fact, it can be applied to any non-static, non-private, non-final, non-void and parameterless method!
Next, let's look at the implementation of the Formatter
interface:
@Neuron
interface NeuronFormatter extends Formatter {
@Caching
@Named("format")
String getFormat();
@Override
default String format(Object... args) { return String.format(getFormat(), (Object[]) args); }
}
This should not surprise you anymore:
Again, NeuronFormatter
is an interface because it allows us to use it as a mix-in.
Just as before, the @Neuron
and @Caching
annotations are applied to signal that Neuron DI should take care of lazy
dependency resolution and caching.
The only notable difference is the usage of the @Named("format")
annotation on the synapse method getFormat()
instead of the private field GuiceFormatter.format
.
Next, let's look at the changes required in the Guice module:
class NeuronGreetingModule extends NeuronModule {
@Override
protected void configure() {
bind(Greeting.class).to(NeuronGreeting.class).in(Singleton.class);
bind(Formatter.class).to(NeuronFormatter.class);
bindConstantNamed("format").to("Hello %s!");
bindNeurons(NeuronGreeting.class, NeuronFormatter.class);
}
}
Note the call to the method bindNeurons
:
Unfortunately, as of Guice 4.1.0, you cannot extend it with custom just-in-time bindings, so we have to explicitly
bind our Neuron classes and interfaces using a statement like this.
Without this call, Guice would not know how to create instances of the neuron interfaces NeuronGreeting
and
NeuronFormatter
at runtime.
Removing this call results in the following exception when creating the Guice Injector
in the GreetingModuleTest
:
com.google.inject.CreationException: Guice creation errors:
1) No implementation for global.namespace.neuron.di.guice.java.test.NeuronGreeting was bound.
at global.namespace.neuron.di.guice.java.test.NeuronGreetingModule.configure(NeuronGreetingModule.java:28)
2) No implementation for global.namespace.neuron.di.guice.java.test.NeuronFormatter was bound.
at global.namespace.neuron.di.guice.java.test.NeuronGreetingModule.configure(NeuronGreetingModule.java:29)
2 errors
at com.google.inject.internal.Errors.throwCreationExceptionIfErrorsExist(Errors.java:435)
at com.google.inject.internal.InternalInjectorCreator.initializeStatically(InternalInjectorCreator.java:154)
at com.google.inject.internal.InternalInjectorCreator.build(InternalInjectorCreator.java:106)
at com.google.inject.Guice.createInjector(Guice.java:95)
...
Finally, we can test the NeuronGreetingModule
:
public class NeuronGreetingModuleTest extends GreetingModuleTest {
@Override
protected Module module() { return new NeuronGreetingModule(); }
}