I'm using the pattern of IndirectActorProducer
in my Play Framework app using Akka and Guice dependency injection.
Like this:
import akka.actor.Actor;
import akka.actor.IndirectActorProducer;
import play.api.inject.Injector;
public class GuiceActorProducer implements IndirectActorProducer {
private Injector injector;
private Class<Actor> cls;
public GuiceActorProducer(Injector injector, Class<Actor> cls) {
this.injector = injector;
this.cls = cls;
}
@Override
public Actor produce() {
return injector.instanceOf(cls);
}
@Override
public Class<? extends Actor> actorClass() {
return Actor.class;
}
}
This works very well to create actors that are 100% injected, like this:
ActorRef imageGenerator = actorService.getActorSystem().actorOf(
Props.create(GuiceActorProducer.class, injector, ImageGenerator.class),
String.format("ImageGenerator-%d", brandId));
Unfortunately, I have actors that are 50% injected and 50% "assisted". They have a factory like this:
public interface Factory {
Synchronizer create(@Assisted("taskLogId") Long taskLogId, @Assisted("clientGroup") String clientGroup, @Assisted boolean messagesOnly, @Assisted("notes") String notes);
}
And they are binded in the module like this:
bindActorFactory(Synchronizer.class, SynchronizerProtocol.Factory.class);
When I want to create a Synchronizer actor, I try like this but it doesn't work. There is no error but it doesn't enter in the constructor at all. And it's normal, I don't provide the required parameters...
ActorRef myActor = actorService.getActorSystem().actorOf(
Props.create(GuiceActorProducer.class, injector, Synchronizer.class), synchronizerName);
I don't know where to provide my 4 parameters using this syntax. I'm pretty sure I need to give them to the injector but I can't find any documentation on this nor any function in the api that point me in this direction.
This is the constructor of the Synchronizer just to be clear. It needs to be injected because it have lots of factory to create other objets. Also, the Synchronizer actor needs to be created in a normal object (not an actor) and it needs to be named
@Inject
private Synchronizer(SyncStateErrorUserNotification.Factory syncStateErrorUserNotificationFactory,
NewBrandStationLinkUserNotification.Factory newBrandStationLinkUserNotificationFactory,
MediaUpdaterProtocol.Factory mediaUpdatersFactory,
StationSyncFileGeneratorProtocol.Factory stationSyncFileGeneratorsFactory,
StationSynchronizerProtocol.Factory stationSynchronizerFactory,
BrandSyncFileGeneratorProtocol.Factory brandSyncFileGeneratorsFactory,
PlaylistUpdaterProtocol.Factory playlistUpdatersFactory,
Injector injector,
StationScheduleStyleService stationScheduleStyleService,
LogCreationService logCreationService,
CustomSqlManagerService customSqlManagerService,
SongSynchronizerProtocol.Factory songSynchronizerFactory,
ClientGroupSynchronizerProtocol.Factory clientGroupSynchronizersFactory) {
super(logCreationService);
this.syncStateErrorUserNotificationFactory = syncStateErrorUserNotificationFactory;
this.newBrandStationLinkUserNotificationFactory = newBrandStationLinkUserNotificationFactory;
this.mediaUpdatersFactory = mediaUpdatersFactory;
this.playlistUpdatersFactory = playlistUpdatersFactory;
this.injector = injector;
this.stationScheduleStyleService = stationScheduleStyleService;
this.logCreationService = logCreationService;
this.customSqlManagerService = customSqlManagerService;
this.stationSyncFileGeneratorsFactory = stationSyncFileGeneratorsFactory;
this.brandSyncFileGeneratorsFactory = brandSyncFileGeneratorsFactory;
this.stationSynchronizerFactory = stationSynchronizerFactory;
this.songSynchronizerFactory = songSynchronizerFactory;
this.clientGroupSynchronizersFactory = clientGroupSynchronizersFactory;
}
Help! Thanks
AssistedInject takes place when a constructor of injected class has not-injected arguments. Guice cannot create such instance on his own.
A factory comes to assist Guice in creating of instances: this factory is an interface, which has a method (or several methods) that receives all not-injected parameters and returns an appropriate instance.
The factory interface is installed in the module instead of binding a class. The factory is injected instead of a class instance; it is used for creating of a class instance. You can see here more detailed explanation and examples.
Akka way for assisted inject is IndirectActorProducer. Play makes some things under the veil and suggests another pattern for assisted inject, like it is explained in the documentation.
I personally found that using assisted inject for actors in Play demands too much boilerplate code and it should be used as a last choice. I will show both options: how to inject actor with non-injected parameters (aka via assisted inject) and how to create it with Props.
The Synchronizer has the Factory interface, which is called for injecting an actor instance instead of Props:
public class Synchronizer extends UntypedActor {
private final Long taskLogId;
private final String clientGroup;
private final boolean messagesOnly;
private final String notes;
@Inject
public Synchronizer(@Assisted Long taskLogId, @Assisted("clientGroup") String clientGroup, @Assisted boolean messagesOnly, @Assisted("notes") String notes) {
this.taskLogId= taskLogId;
this.clientGroup= clientGroup;
this.messagesOnly= messagesOnly;
this.notes= notes;
}
@Override
public void onReceive(Object message) throws Exception {
...
}
// The factory
public interface Factory {
public Actor create(Long taskLogId, String clientGroup, boolean messagesOnly, String notes);
}
}
Play demands a parent actor, which will provide assisted inject of a child actor. Let's say, it is SynchronizerParent. The parent injects a child upon receiving of a CreateSynchronizer message, which encapsulates all parameters needed for Synchronizer creation. It sends the ActorRef of the injected child to the caller:
public class SynchronizerParent extends UntypedActor implements InjectedActorSupport {
//Protocol
public static class CreateSynchronizer {
private final Long taskLogId;
private final String clientGroup;
private final boolean messagesOnly;
private final String notes;
private final String brandId;
public CreateSynchronizer(
Long taskLogId, String clientGroup,
boolean messagesOnly, String notes, String brandId) {
this.taskLogId = taskLogId;
this.clientGroup = clientGroup;
this.messagesOnly = messagesOnly;
this.notes = notes;
this.brandId= brandId;
}
}
private Synchronizer.Factory childFactory;
@Inject
public SynchronizerParent(Synchronizer.Factory childFactory) {
this.childFactory = childFactory;
}
@Override
public void onReceive(Object message) throws Exception {
if (message instanceof CreateSynchronizer) {
injectSynchronizer((CreateSynchronizer)message);
}
else {
unhandled(message);
}
}
private void injectSynchronizer(CreateSynchronizer injectMsg) {
ActorRef child = injectedChild(() -> childFactory.create(
injectMsg.taskLogId,
injectMsg.clientGroup,
injectMsg.messagesOnly,
injectMsg.notes)
, "child-" +injectMsg.brandId);
sender().tell(child, self());
}
}
Bindings in the module (which should implement AkkaGuiceSupport):
public class ActorsModule extends AbstractModule implements AkkaGuiceSupport {
@Override
protected void configure() {
bindActor(SynchronizerParent.class, "parentActor");
bindActorFactory(Synchronizer.class, Synchronizer.Factory.class);
}
}
Actors service uses ask pattern: it sends CreateSynchronizer message to the parent actor and expects to receive back the ActorRef to the injected child:
public class ActorsServiceImpl extends ActorsService {
private ActorRef parentActor;
@Inject
public ActorsServiceImpl(@Named("parentActor") ActorRef parentActor) {
this.parentActor = parentActor;
}
public CompletionStage<ActorRef> createSyncronizer(Long taskLogId, String clientGroup, boolean messagesOnly, String notes, String brandId) {
// Use guice assisted injection to instantiate and configure the child actor.
long timeoutMillis = 100L;
return FutureConverters.toJava(
ask(parentActor,
new SynchronizerParent.CreateSynchronizer(taskLogId, clientGroup, messagesOnly, notes),
brandId),
timeoutMillis))
.thenApply(response -> (ActorRef) response);
}
}
Syncronizer now contains a static factory method props instead of the Factory interface:
public class Synchronizer extends UntypedActor {
private final Long taskLogId;
private final String clientGroup;
private final boolean messagesOnly;
private final String notes;
// Factory method
public static Props props(Long taskLogId, String clientGroup, boolean messagesOnly, String notes) {
return Props.create(Synchronizer.class, taskLogId, clientGroup, messagesOnly, notes);
}
public Synchronizer(Long taskLogId, String clientGroup, boolean messagesOnly, String notes) {
this.taskLogId= taskLogId;
this.clientGroup= clientGroup;
this.messagesOnly= messagesOnly;
this.notes= notes;
}
@Override
public void onReceive(Object message) throws Exception {
...
}
}
Actors Service implementation:
public class ActorsServiceImpl extends ActorsService {
...
public ActorRef createSynchronizer(Long taskLogId, String clientGroup,
boolean messagesOnly, String notes, String brandId) {
return getActorSystem().actorOf(Synchronizer.props(
taskLogId, clientGroup, messagesOnly, notes),
"ImageGenerator-" + brandId);
}
}
I definitely prefer to use the second option.