Skip to content

NeuronDIAtGuiceForJavaTutorial

Christian Schlichtherle edited this page Dec 10, 2018 · 4 revisions

Neuron DI @ Guice For Java - Tutorial

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.

Pure Guice

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.

Neuron DI @ Guice

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(); }
}
Clone this wiki locally