Skip to content
homedirectory edited this page Jun 28, 2024 · 16 revisions

Guice

This document contains guidelines for using Guice in TG.

Guice Concepts

This section outlines concepts and terminology related to Guice. It should be used as a reference for the rest of the document.

Glossary

  • Injector - an instance of com.google.inject.Injector, the core abstraction in Guice, literally injects your dependencies.

  • Module - a subtype of com.google.inject.Module, serves as a means of configuring an injector, is composed of bindings.

  • Binding - an association between a key and a provider.

  • Key - describes a dependency, typically a Java class or an interface.

  • Provider - is bound to a key, provides a dependency described by that key, typically an instance of a class/interface.

  • Explicit binding - a binding that is explicitly declared in a module.

    class MyModule extends com.google.inject.AbstractModule {
      void configure() {
        bind(IDates.class).to(DefaultDates.class);
      }
    }
  • Implicit binding (aka Just-in-time binding) - a binding that is not declared in a module; these come in 2 forms:

    1. Any type with an @Inject-annotated constructor or a nullary (no-arg) constructor.
    2. A type specified in a @ImplementedBy annotation (aka ImplementedBy binding).
  • Provider - a computation that produces a value as if it was created by Guice.

Reference Points

These are referred below in the form of regular expression ref n(,m)*, where n and m are reference numbers.

  1. The same key cannot be explicitly bound twice in the same injector configuration, even if those bindings appear in different modules.
  2. Explicit bindings override implicit ones.

Singleton Scope

There are 2 ways of specifying the Singleton scope:

  1. Annotate the class with @Singleton.
  2. Use the DSL: bind(...).to(...).in(Scopes.SINGLETON)

@Singleton annotation is strongly preferred for the following reasons:

  1. Better evolvability

    If the class is modified in such a way that the singleton scope is no longer fitting, the author of the change is more likely to think about the scope if the annotation is present in that same class; otherwise, they would need to find all explicit bindings of that class and adjust the respective scopes.

  2. It avoids confusion if the class is bound in several modules with different scopes

    Imagine a scenario where a class is bound in modules A and B, with Singleton scope in the former and no scope in the latter. What does this mean? Did the author of B simply forget to specify the Singleton scope? Or was this a deliberate choice? Unless there is a comment that explains it, confusion is guaranteed to take place.

Also, the @Singleton scope can be overriden by specifying NO_SCOPE with an explicit binding (see more).

Guice in TG

In TG Guice modules are organised in a type hierarchy. An Injector is obtained by passing the most specific module to ApplicationInjectorFactory and optionally providing additional modules.

Example:

var module = new WebApplicationServerModule(...);
injector = new ApplicationInjectorFactory()
        .add(module)
        .add(new NewUserEmailNotifierBindingModule())
        .add(new EmailSenderModule())
        .getInjector();

Platform-level Modules

When designing platform-level modules the following is recommended:

1. Provide default implementations for interfaces that are exposed to applications

Providing a default implementation frees the module extender from the burden of configuring an explicit binding for it by themselves.

How to configure bindings for default implementations? There are 2 cases:

  1. Using @ImplementedBy. This approach, as opposed to an explicit binding, allows the module extender to freely bind their own implementation if they wish to override the default one. See ref 1,2.

  2. The default implementation is not accessible to the interface (e.g., due to limited visibility access or residing in different Maven modules), eliminating the possibility of using @ImplementedBy.

    This case is a problematic one for Guice. There are several approaches to binding the default implementation:

    1. Explicit binding. Suffers from ref 1, requires the use Modules.override, additional cognitive strain.

    2. Module constructor parameter. Slightly better than 1 due to being immediately visible. Worse because of reduced ergonomics. Also, expect combinatorial explosion of constructors as the number of such bindings increases.

      class MyModule extends AbstractModule {
        final Class<? extends Contract> contractImpl;
      
        // accepts user-supplied implementation
        MyModule(Class<? extends Contract> contractImpl) {
          this.contractImpl = contractImpl;
        }
      
        // default implementation
        MyModule() {
          this(DefaultContractImpl.class);
        }
      
        void configure() {
          bind(Contract.class).to(contractImpl);
        }
      }
    3. Module methods that can be overriden. Similar to 2, but less visible and increased cognitive strain.

      class MyModule extends AbstractModule {
        void configure() {
          bindContract();
        }
      
        protected void bindContract() {
          bind(Contract.class).to(DefaultContractImpl.class);
        }
      }

    Yet another approach is to embrace Modules.override for all modules, i.e., before creating an injector override modules sequentially on top of each other. This is by far the most concise and ergonomic approach, but it makes debugging more difficult.

2. Use requireBinding for keys that are expected be bound

com.google.inject.AbstractModule#requireBinding instructs Guice to check that the specified key has been bound when creating an injector.

This practice strengthens the modules and assists in detecting misconfigurations.

Note that a constraint imposed by requireBinding can be satisfied both by an explicit and an implicit binding.

Overriding Module Bindings

Overriding a binding from any module in the hierarchy can be achieved as follows:

var mostSpecificModule = ...;
var newModule = com.google.inject.util.Modules.override(mostSpecificModule)
  .with(new AbstractModule() {
    @Override
    protected void configure() {
        // overrides go here
    }
});
// from here onwards use newModule

Module Design Best Practices

1. Make modules customisable

If a module contains dynamic behaviour that is responsible for bindings things, consider putting that code in a protected method and calling that method in configure(). This will allow application-specific modules to have control over that behaviour.

Example: binding companion objects by iterating over application domain entity types.

2. Hide implementations

Consider the following project structure:

world
| find
  | IFinder    (public)
  | FinderImpl (package-private)
| ioc
  WorldModule
  • WorldModule - a Guice module.
  • IFinder - an interface that needs to be bound to its implementation FinderImpl.

Question: how to bind IFinder to FinderImpl while keeping the implementation hidden (package-private)?

Establishing the binding in ioc.WorldModule will require world.find.FinderImpl to become public, since they reside in different packages.

There are 2 viable approaches:

  1. Using @ImplementedBy.
  2. Using a package-local module.
@ImplementedBy

Simply annotate the interface with information about the implementation class:

// world.find.IFinder
@ImplementedBy(FinderImpl.class)
public interface IFInder {}

// world.find.FinderImpl
class FinderImpl implements IFinder {}

This approach is preferred in most cases as it is far more concise. On the other hand, if non-trivial construction logic is required for the implementation class, consider using a package-local module.

Package-local modules

Create world.find.FinderModule which will contain the binding and preserve the package-private visibility of FinderImpl. Then, this module can be installed in WorldModule.

// world.ioc.WordModule
public class WordModule extends AbstractModule {
  void configure() {
    install(new FinderModule());
    // other bindings...
  }
}

// world.find.FinderModule
public class FinderModule extends AbstractModule {
  void configure() {
    bind(IFinder.class).to(FinderImpl.class);
    // other bindings...
  }
}

// world.find.IFinder
public interface IFinder {}

// world.find.FinderImpl
// doesn't need to be public!
class FinderImpl implements IFinder {}

3. Interceptors with dependencies

Besides dependency injection, Guice also provides support for AOP in the form of method interceptors which can be bound in modules.

In some scenarios an interceptor has dependencies which we would like Guice to inject. The way to do this, however, is not as straightforward as with ordinary injections since an interceptor instance needs to be created inside a module's configure method (i.e., before the Injector is created). So, to achieve injection of an interceptor, Provider<T> can be used, which represents a computation that provides a dependency specified by type variable T. A provider instance can be obtained inside a module's configure method via getProvider(Class).

Keep in mind that you must not call the provider until the injector has been created, otherwise it will lead to failure.

Example:

// MyInterceptor.java
class MyInterceptor implements org.aopalliance.intercept.MethodInterceptor {

  final Provider<Dependency> dependencyProvider;

  MyInterceptor(Provider<Dependency> dependencyProvider) {
    // must not call the provider here, as the injector hasn't been created yet
    this.dependencyProvider = dependencyProvider;
  }

  public Object invoke(MethodInvocation invocation) {
    // use the dependency
    Dependency dependency = dependencyProvider.get();
    // ...
  }
}

// MyModule.java
class MyModule extends AbstractModule {
  void configure() {
    bindInterceptor(Matchers.any(), 
                   annotatedWith(MyAnnotation.class), 
                   new MyInterceptor(getProvider(Dependency.class)));
  }
}

Combining injected and non-injected parameters

In some cases a class has both injectable dependencies and ones that are known only at the call site.

Example:

public class DeleteOperation {
  @Inject
  public DeleteOperation(Metadata metadata, // injectable
                         @Assisted Session session // known at the call site
  ) {...}
}

In such cases we can user neither new nor ask Guice to construct the object, we need to combine both. Assisted Inject attempts to solve this problem through the usage of factories. Essentially, for each class that needs a combination of both injectable and call site dependencies, there is a factory class that:

  1. Contains state composed only of injectable dependencies.
  2. Declares a method that accepts call site dependencies and constructs the required object by combining both kinds of dependencies.
public class DeleteOperationFactory {
  final Metadata metadata;

  @Inject
  public DeleteOperationFactory(Metadata metadata) {
    this.metadata = metadata;
  }

  public DeleteOperation create(Session session) {
    return new DeleteOperation(method, session);
  }
}

To demonstrate factory usage, consider class Controller that depends on DeleteOperation:

public class Controller {
  @Inject DeleteOperationFactory deleteOpFactory;

  void delete(...) {
    Session session = ...;
    DeleteOperation deleteOp = deleteOpFactory.create(session);
  }
}

Assisted Inject explains how to bind factories.

Generic factory methods

There is a caveat with Assisted Inject: factories that have generic methods must be written out by hand because FactoryModuleBuilder doesn't support such cases.

Example:

interface StoreFactory {
  <T> Store<T> create(T item);
}

// DOES NOT WORK
// Produces error message:
// "StoreFactory cannot be used as a key; It is not fully specified"
install(new FactoryModuleBuilder().build(StoreFactory.class));

The solution is to implement the factory by hand. The interface can be skipped alltogether, which has the benefit of not requiring an explicit binding. For example:

class StoreFactory {
  // fields for injectable dependencies...

  public <T> Store<T> create(T item) {
    return new Store(/*injectable dependencies...*/, item);
  }
}

Then this factory can be requested directly, without binding it explicitly (see Just-In-Time Bindings).

Miscellaneous

Various useful bits of information about Guice.

Binding Resolution

Guice will use a single @Inject constructor or a public no-arg constructor.

Clone this wiki locally