home / 2018.04.14 10:00 /scala /spring boot /akka /dependency injection
The actor paradigm is useful in building and scaling a
highly-parallelized application, but it makes sense to move the actual
code that does the heavy computation outside the actors, in some
auxiliary services. In addition, actors need to get access to outside
resources, like database connections and API connections. You could just
provide all those services and connections to each actor type when
creating it. Or you could use some framework that provides dependency
injection, like Spring, to just declare what your actor dependencies are
and stop worrying about propagating them down the actor hierarchy
through the props
factory.
To be able to inject Spring dependencies into our actors we’ll have Akka use a special “producer” when creating them. This producer will load the new actors out of the Spring context.
package com.cacoveanu.scalademo
import akka.actor.{Actor, IndirectActorProducer}
import org.springframework.context.ApplicationContext
class SpringActorProducer(private val applicationContext: ApplicationContext,
private val actorBeanName: String) extends IndirectActorProducer {
override def produce(): Actor = applicationContext.getBean(actorBeanName).asInstanceOf[Actor]
override def actorClass: Class[_ <: Actor] = classOf[Actor]
}
A new producer will be used for each new actor type we add to the
system. We use a spring extension class to call the props factory and
create a new actor in the actor system, but we provide our actor
producer to the props factory. This way, the props factory will call the
produce
method in our producer when it wants to create a new actor, and the produce
method will grab the actor from the Spring context, which means that dependencies inside the actor will be autowired.
package com.cacoveanu.scalademo
import akka.actor.{Extension, Props}
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.stereotype.Component
import scala.beans.BeanProperty
@Component
class SpringExtension extends Extension {
@Autowired @BeanProperty val applicationContext: ApplicationContext = null
def props(actorBeanName: String): Props =
Props.create(classOf[SpringActorProducer], applicationContext, actorBeanName)
}
We’ll need a test service in our Spring context.
package com.cacoveanu.scalademo
import org.springframework.stereotype.Component
@Component
class TestService {
def testItWorks(message: String) = println(message)
}
And a test actor that uses the service. What’s important in the actor
definition is that it gets inserted in the Spring context as a @Component
and it gets scoped as @Scope("prototype")
. The protorype
means that everytime the producer will ask the spring context for a TestInjectionActor
, Spring will create a new instance of that class and inject dependencies on it.
package com.cacoveanu.scalademo
import akka.actor.Actor
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.{Bean, Scope}
import org.springframework.stereotype.Component
import scala.beans.BeanProperty
case class VerifyInjectionWorks(val message: String)
@Component
@Scope("prototype")
class TestInjectionActor extends Actor {
@Autowired @BeanProperty val testService: TestService = null
override def receive: Receive = {
case VerifyInjectionWorks(message) => testService.testItWorks(message)
}
}
Next we need to initalize the system and the actor in our Spring configuration.
@Configuration
class AkkaSystem {
@Bean
def createSystem(springExtension: SpringExtension) : ActorSystem = {
ActorSystem("helloAkka")
}
@Bean(Array("injectionActor"))
def createInjectionActor(system: ActorSystem, springExtension: SpringExtension) = {
system.actorOf(springExtension.props("testInjectionActor"), "injectionActor")
}
}
We also need a reference to the actor in our Spring context so we can inject it into our controller and pass messages to the actor on certain requests.
@Controller
class TestController @Autowired() (
@Qualifier("injectionActor") private val injectionActor: ActorRef
) {
@RequestMapping(Array("/injection"))
@ResponseBody
def testInjection(): String = {
injectionActor ! VerifyInjectionWorks("injection works!")
"verified injection"
}
}
Start your app and go to localhost /injection
to test this out, you should see the injection works!
message in your console. Well, there you go!
If you want to keep injecting your services in child actors (actors
created by your actors), all you need to do is keep using the SpringExtension
when you create them. We’ll adapt our actor to accept a second message
type and create a new injection actor when it receives that message.
case class VerifyInjectionWorks(val message: String)
case class VerifyInjectionWorksInDepth(val message: String)
@Component
@Scope("prototype")
class TestInjectionActor extends Actor {
@Autowired @BeanProperty val testService: TestService = null
@Autowired @BeanProperty val springExtension: SpringExtension = null
override def receive: Receive = {
case VerifyInjectionWorks(message) => testService.testItWorks(message + " " + this.toString)
case VerifyInjectionWorksInDepth(message) => {
context.actorOf(springExtension.props("testInjectionActor")) ! VerifyInjectionWorks(message)
}
}
}
Next we’ll have our controller send both messages to the actor.
@RequestMapping(Array("/injection"))
@ResponseBody
def testInjection(): String = {
injectionActor ! VerifyInjectionWorks("injection works!")
injectionActor ! VerifyInjectionWorksInDepth("injection works in depth!")
"verified injection"
}
If you start the app and call the /injection
endpoint twice you should see the following in the console:
injection works! com.cacoveanu.scalademo.TestInjectionActor@7165bf7f
injection works in depth! com.cacoveanu.scalademo.TestInjectionActor@7fa25f23
injection works! com.cacoveanu.scalademo.TestInjectionActor@7165bf7f
injection works in depth! com.cacoveanu.scalademo.TestInjectionActor@648fb385
Why twice? If you look at the actor names you’ll see that the base actor and the child actor have different IDs; also the child actor changes between calls because it is created by the base actor. And, of course, what we were interested in in the first place, the injection works for actors in the hierarchy as well if we use the spring extension.
With injection, your actor code should become cleaner and a lot more manageable. You can write the heavy processing code in services that you inject in the actors, and let the actors code focus on how the parallelization of your application is organized.