Home Exploring the World of Guice Binding
Post
Cancel

Exploring the World of Guice Binding

Google Guice is a popular dependency injection framework for Java that makes it easy to manage the relationships between objects in your application. Guice allows you to declare the dependencies between objects, which eliminates the need for manual object creation and management. This helps to make your code more readable, maintainable and testable. In this blog, we will focus on one of its key features: Binding.

A binding is an object that corresponds to an entry in the Guice map. To create bindings, you need to extend AbstractModule and override its configure method. Once you’ve created your modules, pass these modules as argument to Guice.createInjector() to build an injector, which can be used to bootstrap your application later. For example:

1
2
Injector injector = Guice.createInjector(new ApplicationModule());
Application application = injector.getInstance(Application.class);

I will walk you through all popular binding methods in this blog. Now let’s go!

Linked Binding

Linked binding maps an interface to its implementation, or a superclass to a subclass. Let’s take an example. At first, we create an interface Shape and then create an implementation Circle using this interface.

1
2
3
public interface Shape {
    void print();
}
1
2
3
4
5
6
public class Circle implements Shape {
    @Override
    public void print() {
        System.out.println("This is a circle!");
    }
}

You can bind Shape interface to Circle implementation by adding following code in the Guice Module class:

1
2
3
4
5
6
public class ApplicationModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(Shape.class).to(Circle.class);
    }
}

Then when you call injector.getInstance(Shape.class), or when the injector encounters a dependency on Shape, it will use a Circle. Linked bindings can also be chained.

Binding Annotation

Sometimes you have multiple bindings for the same type. To enable this, Guice supports an optional binding annotation. The annotation and type together uniquely identify a binding. This pair is called a key.

Let’s say, you have 2 implementations of Shape interface: Circle and Square, and you want to create bindings for both. Then,binding annotations can help. There are 2 ways of using binding annotations.

Create a Java annotation

Defining a binding annotation requires several imports.

1
2
3
4
@Qualifier
@Target({ FIELD, PARAMETER, METHOD })
@Retention(RUNTIME)
public @interface CircleShape {}
1
2
3
4
@Qualifier
@Target({ FIELD, PARAMETER, METHOD })
@Retention(RUNTIME)
public @interface SquareShape {}

In the example above, we create 2 different java annotations: CircleShape and SquareShape. When you want to bind Circle implementation, annotate the binding with CircleShape. Similarly, annotate the binding with SquareShape to bind Square implementation.

1
2
3
4
5
6
7
public class ApplicationModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(Shape.class).annotatedWith(CircleShape.class).to(Circle.class);
        bind(Shape.class).annotatedWith(SquareShape.class).to(Square.class);
    }
}

To depend on the annotated binding, you can either apply the annotation in injector.getInstance function:

1
2
Shape circle = injector.getInstance(Key.get(Shape.class, CircleShape.class));
Shape square = injector.getInstance(Key.get(Shape.class, SquareShape.class));

or add it to the injected parameter:

1
2
3
4
@Inject
public Service(@CircleShape Shape shape) {
  ...
}

@Named

Another way to distinguish bindings is @Named that takes a string:

1
2
3
4
5
6
7
public class ApplicationModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(Shape.class).annotatedWith(Names.named("Circle")).to(Circle.class);
        bind(Shape.class).annotatedWith(Names.named("Square")).to(Square.class);
    }
}

Since java compiler can’t check the string, using Named is error-prone. Please use Named carefully.

Instance Binding

You can bind a type to a specific instance of the type. This is usually only useful for objects that don’t have dependencies of their own, such as value objects.

1
2
3
4
5
6
7
public class ApplicationModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(String.class).annotatedWith(Names.named("Name")).toInstance("Kenny");
        bind(Integer.class).annotatedWith(Names.named("ID")).toInstance(123);
    }
}

In the example above, when you instantiate a String object, it will ‘Kenny’. And 123 will be assigned to Integer in the Guice injector.

Please note that you should avoid using .toInstance with objects that are complicated to create, since it can slow down application startup. Instead, you can use an @Provides method, which I will cover later.

Provider Binding

Guice allows you to specify a provider class that creates instances of a particular types. This can be useful when you need to create instances of a class that require additional configuration or setup.

The Provider class needs to implement implements the javax.inject.Provider interface. The get() method is responsible for creating and returning the instance.

1
2
3
4
5
6
7
8
public class CircleProvider implements Provider<Shape> {
    public Shape get() {
        Circle circle = new Circle();
        // Perform additional set up for circle instance
        ...
        return circle;
    }
}

Then, you can create a binding using this Provider class in the Guice module class:

1
2
3
4
5
6
public class ApplicationModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(Shape.class).toProvider(CircleProvider.class);
    }
}

@Provides

@Provides is powerful to create bindings, because it can almost satisfy all use cases mentioned in previous sections. You gain more control over the instantiation process of a class by @Provides.

Instead of using bind by overriding configure() method, you can create functions annotated by @Provides. The method’s return type is the bound type. Whenever the injector needs an instance of that type, it will invoke the method. Binding annotation can also be applied to distinguish implementations of the same type.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ApplicationModule extends AbstractModule {
    @Override
    protected void configure() {
        ...
    }

    @Provides
    @CircleShape
    Shape providesCircle() {
        return new Circle();
    }

    @Provides
    @SquareShape
    Shape providesSquare() {
        return new Square();
    }

}

This is my preferred option to create bindings, because you do not need to remember which bind should be used in configure(). All you need is to define every instantiation details inside the function annotated with @Provides.

Just-in-time Binding

In addition to explicitly creating bindings in the Guice module class, bindings can also be created implicitly. If a type is needed but there isn’t an explicit binding, the injector will attempt to create a Just-In-Time binding.

@Inject Constructors

This is commonly seen to create JIT(Just-In-Time) bindings. You simply need to annotate the constructor of the class that you want to bind with the @Inject annotation. For example:

1
2
3
4
5
6
7
8
9
public class Application {
    private Shape circle;

    @Inject
    public Application(@CircleShape Shape circle) {
        this.circle = circle;
    }

    ...

Besides using @Inject constructors, a special case exists when constructors take zero arguments, and is non-private and defined in a non-private class. Although a class does not have an annotated constructor, it can also be injected if a zero-argument constructor is created.

@ImplementedBy and @ProvidedBy

Implicit bindings are created when a class is annotated by either @ImplementedBy or @ProvidedBy. For example,

1
2
3
4
@ImplementedBy(Circle.class)
public interface Shape {
    void print();
}

or

1
2
3
4
@ProvidedBy(CircleProvider.class)
public interface Shape {
    void print();
}

@ImplementedBy annotation is equivalent to the following bind() statement:

1
bind(Shape.class).to(Circle.class);

@ProvidedBy annotation is equivalent to the following bind() statement:

1
bind(Shape.class).toProvider(CircleProvider.class);

Enforce Explicit Bindings

To disable implicit bindings, you can use the requireExplicitBindings API:

1
2
3
4
5
6
public class ApplicationModule extends AbstractModule {
    @Override
    protected void configure() {
      binder().requireExplicitBindings();
    }
}

After enabling this, all bindings must be listed in a Module class in order to be injected.

This post is licensed under CC BY 4.0 by the author.

How To Mount NVMe SSDs on EC2 instances

-

Comments powered by Disqus.