NejcT
NejcT

Reputation: 167

Guice multiple implementations, parameterized constructor with dependencies

I'm struggling with a particular dependency injection problem and I just can't seem to figure it out. FYI: I'm new to guice, but I have experience with other DI frameworks - that's why I believe this shouldn't be to complicated to achieve.

What am I doing: I'm working on Lagom multi module project and using Guice as DI.

What I would like to achieve: Inject multiple named instances of some interface implementation (lets' call it publisher, since it will publishing messages to kafka topic) to my service. This 'publisher' has injected some Lagom and Akka related services (ServiceLocator, ActorSystem, Materializer, etc..).

Now I would like to have two instances of such publisher and each will publish messages to different topic (So one publisher instance per topic).

How would I achieve that? I have no problem with one instance or multiple instances for the same topic, but if I want to inject different topic name for each instance I have a problem.

So my publisher implementation constructor looks like that:

@Inject
public PublisherImpl(
    @Named("topicName") String topic,
    ServiceLocator serviceLocator,
    ActorSystem actorSystem,
    Materializer materializer,
    ApplicationLifecycle applicationLifecycle) {
...
}

If I want to create one instance I would do it like this in my ServiceModule:

public class FeedListenerServiceModule extends AbstractModule implements ServiceGuiceSupport {
    @Override
    protected void configure() {
        bindService(MyService.class, MyServiceImpl.class);
        bindConstant().annotatedWith(Names.named("topicName")).to("topicOne");
        bind(Publisher.class).annotatedWith(Names.named("publisherOne")).to(PublisherImpl.class);
    }
}

How would I bind multiple publishers each for it's own topic?

I was playing around with implementing another private module:

public class PublisherModule extends PrivateModule {

    private String publisherName;
    private String topicName;

    public PublisherModule(String publisherName, String topicName) {
        this.publisherName = publisherName;
        this.topicName = topicName;
    }

    @Override
    protected void configure() {
        bindConstant().annotatedWith(Names.named("topicName")).to(topicName);
        bind(Publisher.class).annotatedWith(Names.named(publisherName)).to(PublisherImpl.class);
    }
}

but this led me nowhere since you can't get injector in you module configuration method:

Injector injector = Guice.createInjector(this); // This will throw IllegalStateException : Re-entry is not allowed
injector.createChildInjector(
    new PublisherModule("publisherOne", "topicOne"),
    new PublisherModule("publisherTwo", "topicTwo"));

The only solution which is easy and it works is that I change my PublisherImpl to abstract, add him abstract 'getTopic()' method and add two more implementations with topic override.

But this solution is lame. Adding additional inheritance for code reuse is not exactly the best practice. Also I believe that Guice for sure must support such feature.

Any advises are welcome. KR, Nejc

Upvotes: 1

Views: 3103

Answers (2)

Jeff Bowman
Jeff Bowman

Reputation: 95634

Don't create a new Injector within a configure method. Instead, install the new modules you create. No child injectors needed—as in the PrivateModule documentation, "Private modules are implemented using parent injectors", so there's a child injector involved anyway.

install(new PublisherModule("publisherOne", "topicOne"));
install(new PublisherModule("publisherTwo", "topicTwo"));

Your technique of using PrivateModule is the one I'd go with in this situation, particularly given the desire to make the bindings available through binding annotations as you have it, and particularly if the full set of topics is known at runtime. You could even put the call to install in a loop.

However, if you need an arbitrary number of implementations, you may want to create an injectable factory or provider to which you can pass a String set at runtime.

public class PublisherProvider {
  // You can inject Provider<T> for all T bindings in Guice, automatically, which
  // lets you configure in your Module whether or not instances are shared.
  @Inject private final Provider<ServiceLocator> serviceLocatorProvider;
  // ...

  private final Map<String, Publisher> publisherMap = new HashMap<>();

  public Publisher publisherFor(String topicName) {
    if (publisherMap.containsKey(topicName)) {
      return publisherMap.get(topicName);
    } else {
      PublisherImpl publisherImpl = new PublisherImpl(
          topicName, serviceLocatorProvider.get(), actorSystemProvider.get(),
          materializerProvider.get(), applicationLifecycleProvider.get());
      publisherMap.put(topicName, publisherImpl);
      return publisherImpl;
    }
  }
}

You'd probably want to make the above thread-safe; in addition, you can avoid the explicit constructor call by using assisted injection (FactoryModuleBuilder) or AutoFactory, which will automatically pass through explicit parameters like topicName while injecting DI providers like ServiceLocator (which hopefully has a specific purpose, because you may not need much service-locating within a DI framework anyway!).

(Side note: Don't forget to expose your annotated binding for your PrivateModule. If you don't find yourself injecting your topicName anywhere else, you might also consider using individual @Provides methods with the assisted injection or AutoFactory approach above, but if you expect each Publisher to need a differing object graph you might choose the PrivateModule approach anyway.)

Upvotes: 4

James Roper
James Roper

Reputation: 12850

Guice's approach to dependency injection is that the DI framework complements your instantiation logic, it doesn't replace it. Where it can, it will instantiate things for you, but it doesn't try to be too clever about it. It also doesn't confuse configuration (topic names) with dependency injection - it does one thing, DI, and does that one thing well. So you can't use it to configure things, the way you can with Spring for example.

So if you want to instantiate an object with two different parameters, then you instantiate that object with two different parameters - ie, you invoke new twice. This can be done by using provider methods, which are documented here:

https://github.com/google/guice/wiki/ProvidesMethods

In your case, it might look something like adding the following method to your module:

@Provides
@Named("publisherOne")
@Singleton
Publisher providePublisherOne(ServiceLocator serviceLocator,
    ActorSystem actorSystem,
    Materializer materializer,
    ApplicationLifecycle applicationLifecycle) {
  return new PublisherImpl("topicOne", serviceLocator, 
      actorSystem, materializer, applicationLifecycle);
}

Also, you probably want it to be a singleton if you're adding a lifecycle hook, otherwise you could run into memory leaks each time you add a new hook every time it's instantiated.

Upvotes: 1

Related Questions