BT

Le Trio : Akka, Spring et Scala

Écrit par Slim Ouertani le 16 oct. 2013 |

Introduction

Akka est un framework OpenSource soutenu par TypeSafe, disponible à la fois en Scala et en Java. Il permet de gérer efficacement des applications concurrentes et encourage la programmation réactive et événementielle. Cependant, Spring est une boîte à outils très riche considérée comme un conteneur dit « léger », c'est-à-dire une infrastructure similaire à un serveur d'applications JEE. Elle prend en charge la création d'objets et leur mise en relation par l'intermédiaire d’une configuration qui décrit les objets à fabriquer et les relations de dépendance entre eux.

Réunir le trio Spring, Akka et Scala dans une application est le challenge que nous allons essayer de découvrir tout au long de cet article. On va avoir recours à un nouveau projet souche : Spring-Scala. Ce dernier est conçu pour simplifier l'utilisation de Spring au sein d’une solution développée en Scala.

Dans cet article, nous allons étudier un cas assez simple où vous serez guidés pour intégrer Akka avec Spring-Scala, injecter des dépendances dans nos acteurs Akka et tester nos services avec un wrapper à injecter pendant cette phase sans aucune modification des codes source.

Pré-requis

Notre projet se base sur l'outil de build Sbt. Il sera plus simple de décrire les dépendances à travers sa DSL :

scalaVersion := "2.10.2"

resolvers += "SpringSource Milestone Repository" at "http://repo.springsource.org/milestone"

libraryDependencies ++= Seq(
  "com.typesafe.akka" %% "akka-actor" % "2.2.0",
  "org.springframework.scala" % "spring-scala" % "1.0.0.M2",
  "javax.inject" % "javax.inject" % "1",
  "junit" % "junit" % "4.11" % "test",
  "org.specs2" %% "specs2" % "1.13" % "test"  ,
  "com.novocode" % "junit-interface" % "0.9" % "test->default"
)

Il est à noter que les sources sont également disponibles en tant que modèle Activator.

Modélisation du problème

Exemple à implémenter

Nous allons créer un programme qui permet d'incrémenter les nombres entiers, comportant un acteur qui délègue l’incrémentation à un service injecté via Spring.

AKKA

CountingService

C’est un service trivial qui permet d'incrémenter l’entier qu’il reçoit en paramètre. Ce service est à injecter dans notre acteur CountingActor.

 /**
 * A simple service that can increment a number.
 */
class CountingService {
  /**
   * Increment the given number by one.
   */
  def increment(count: Int) = count + 1
}

CountingActor

Cet acteur sera à l’écoute de deux types de messages COUNT et GET. À la réception du message COUNT, CountingActor délègue l’incrémentation à CountingService et garde le résultat en interne pour le renvoyer en réponse dès la réception du message GET.

object CountingActor {

  object COUNT

  object GET

}

class CountingActor extends Actor {

  import CountingActor._

  var countingService: CountingService = _
  private var count = 0

  def receive = {
    case COUNT => count = countingService.increment(count)
    case GET => sender ! count
  }
}

Extension Akka

Pour être en mesure d’utiliser le contexte d’application et déléguer à Spring le rôle de création et d’injection des dépendances au sein de nos acteurs, il faudra sauvegarder ce contexte dans un emplacement facilement accessible par le moteur d’acteurs Akka. C’est le cas d’utilisation idéal des extensions Akka. Les extensions sont considérées comme des singletons par le système d’acteurs et se décomposent en deux parties dans notre projet :

SpringExtension : définit les méthodes utilisées par Akka pour la création des extensions.

object SpringExtension {
  /**
   * The identifier used to access the SpringExtension.
   */
  def apply() : SpringExtension= new SpringExtension
}

class SpringExtension extends AbstractExtensionId[SpringExtentionImpl] {
    import SpringExtension._
  /**
   * Is used by Akka to instantiate the Extension identified by this
   * ExtensionId, internal use only.
   */
  override def createExtension(system: ExtendedActorSystem) = new SpringExtentionImpl

  /**
   * Java API: retrieve the SpringExt extension for the given system.
   */
  override def get(system: ActorSystem): SpringExtentionImpl = super.get(system)

}

SpringExtentionImpl : créée par l’extension, elle définit les méthodes publiées par l'extension. Cette dernière est créée à travers la méthode apply avec initialisation du contexte Spring alors que la méthode props crée un acteur à partir de son nom.

/**
 * The Extension implementation.
 */
class SpringExtentionImpl extends Extension {
  var applicationContext: ApplicationContext = _

  /**
   * Used to initialize the Spring application context for the extension.
   * @param applicationContext
   */
  def initialize(implicit applicationContext: ApplicationContext) = {
    this.applicationContext = applicationContext
    this
  }

  /**
   * Create a Props for the specified actorBeanName using the
   * SpringActorProducer class.
   *
   * @param actorBeanName The name of the actor bean to create Props for
   * @return a Props that will create the named actor bean using Spring
   */
  def props(actorBeanName: String): Props =
    Props(classOf[SpringActorProducer], applicationContext, actorBeanName)

}

object SpringExtentionImpl {    
  def apply(system : ActorSystem) (implicit ctx: ApplicationContext ) :  SpringExtentionImpl = 
  SpringExtension().get(system).initialize
}

SpringActorProducer

Pour permettre à Spring de créer les acteurs à partir de leurs noms de bean, nous devons trouver un moyen qui permette aux Props Akka de déléguer la création d’acteurs à Spring.

SpringActorProducer implémente l'interface IndirectActorProducer qui est un moyen pour déléguer la création d’acteur à une fabrique. Cette interface possède deux méthodes qui doivent être implémentées :

  • la méthode actorClass qui envoie le type d'acteur qui sera créé.

  • la méthode produce qui crée une nouvelle instance d’acteur à chaque fois qu'elle est appelée.

 


import akka.actor.{Actor, IndirectActorProducer}
import org.springframework.scala.context.function. {FunctionalConfigApplicationContext => FCA }

class SpringActorProducer(ctx: FCA, actorBeanName: String) extends IndirectActorProducer {

    override def produce: Actor = ctx.getBean(actorBeanName, classOf[Actor])

    override def actorClass: Class[_ <: Actor] =
        ctx.getType(actorBeanName).asInstanceOf[Class[_ <: Actor]]
}

 

AppConfiguration

Pour assembler le tout et informer le système d'acteur Akka du contexte de l'application Spring, nous avons une configuration Spring définie dans AppConfiguration qui implémente l’interface FunctionalConfiguration

AppConfiguration dispose de trois méthodes dont la plus importante est actorSystem. Cette méthode est responsable de la création de l’ActorSystem. Le code crée le système d'acteurs ; puis, il initialise SpringExtension avec le contexte d'application Spring requis par SpringActorProducer pour créer des instances d'acteurs à partir des noms de beans.

class AppConfiguration extends FunctionalConfiguration {
  /**
   * Load implicit context
   */
  implicit val ctx = beanFactory.asInstanceOf[ApplicationContext]

  /**
   * Actor system singleton for this application.
   */
  val actorSystem = bean() {
    val system = ActorSystem("AkkaScalaSpring")
    // initialize the application context in the Akka Spring Extension
    SpringExtentionImpl(system)
    system
  }

  val countingService = bean("countingService") {
    new CountingService
  }

  val countingActor = bean("countingActor",  scope = BeanDefinition.SCOPE_PROTOTYPE) {
    val ca = new CountingActor
    ca.countingService = countingService()
    ca
  }
}

Main

Dans cette classe, la fabrique FunctionalConfigApplicationContext est utilisée pour instancier le contexte Spring à partir de notre classe de configuration AppConfiguration et on stimule notre acteur avec 3 envois de messages COUNT. Par la suite, on affiche le résultat obtenu avec l’envoi d’un message GET.

object Main extends App {
  // create a spring context
  implicit val ctx = FunctionalConfigApplicationContext(classOf[AppConfiguration])

  import Config._

  // get hold of the actor system
  val system = ctx.getBean(classOf[ActorSystem])

  val prop = SpringExtentionImpl(system).props("countingActor")

  // use the Spring Extension to create props for a named actor bean
  val counter = system.actorOf(prop, "counter")

  // tell it to count three times
  counter ! COUNT
  counter ! COUNT
  counter ! COUNT

  val result = (counter ? GET).mapTo[Int]
  // print the result
  result onComplete {
    case Success(result) => println(s"Got back $result")
    case Failure(failure) => println(s"Got an exception $failure")
  }

  system.shutdown
  system.awaitTermination
}

TEST

Pour une bonne architecture, où chaque couche logicielle possède une spécification claire, il est facile de lui associer un jeu de tests utilisables quelle que soit la nature de l'implémentation à l’aide de Spring. L'exercice est de mettre en place un test unitaire avec lequel on vérifie qu’on a bien invoqué notre service. Pour ce faire, jetons un coup d’oeil au service TestCountingService. Cette classe hérite de CountingService et redéfinit la méthode increment. Dans cette méthode, on conserve la trace du nombre de fois où elle a été invoquée, en utilisant un compteur interne nommé called ; par la suite, elle délègue le traitement au service réel via super.increment(count).

TestCountingService dispose également d'une méthode nommée getNumberOfCalls qui renvoie la valeur du compteur interne called pouvant servir au cours des tests pour vérifier la manière dont le service est utilisé par l'acteur.

class TestCountingService extends CountingService {
  private val called = new AtomicInteger(0);

  override def  increment( count : Int) = {
    called.incrementAndGet()
    super.increment(count)
  }

  /**
   * How many times we have called this service.
   */
  def getNumberOfCalls()  =  called.get()
}

TestAppConfiguration

Nous tenons également à vérifier dans cette phase que l'acteur a invoqué le service le nombre de fois souhaité en utilisant la méthode getNumberOfCalls. Pour ce faire, on va étendre la classe AppConfiguration et redéfinir countingService pour utiliser notre wrapper de service de test TestCountingService.

class TestAppConfiguration extends AppConfiguration {

  override val countingService = bean("countingService") {
    val cs :  CountingService =  new TestCountingService
    cs
    }
}

SpringTest

Cette classe est équivalente à notre classe Main sauf qu’elle charge le contexte TestAppConfiguration au lieu de AppConfiguration et vérifie en plus que notre service est invoqué à trois reprises avec la méthode getNumberOfCalls.

@RunWith(classOf[JUnitRunner])
class SpringTest extends Specification {

"Simple test" should {

  "Fire 3 COUNT "    in  {
    // create a spring context
    implicit val ctx = FunctionalConfigApplicationContext(classOf[TestAppConfiguration])

    import Config._
    // get hold of the actor system
    val system = ctx.getBean(classOf[ActorSystem])

    val prop = SpringExtentionImpl(system).props("countingActor")

    // use the Spring Extension to create props for a named actor bean
    val counter: ActorRef = system.actorOf(prop, "counter")

    // tell it to count three times
    counter ! COUNT
    counter ! COUNT
    counter ! COUNT

    // check the result
    val result = counter ? GET
    Await.result(result, duration) .asInstanceOf[Int] must beEqualTo(3)
    val  testService = ctx.getBean(classOf[TestCountingService])
    testService.getNumberOfCalls must beEqualTo(3)

    // shut down the actor system
    system.shutdown();
    system.awaitTermination();
  }
 }
}

Conclusion

Nous avons fourni un exemple simple qui montre progressivement l’injection des services ou des services de tests avec Spring-Scala dans un acteur AKKA. Les extensions Akka nous ont servi comme zone de relation entre le système d’acteurs Akka et le conteneur Spring. Pour tirer parti du trio Akka, Spring et Scala, on a aussi expérimenté le framework Spring-Scala qui s’avère prometteur pour simplifier l’introduction des beans Scala dans l’écosystème Spring.

A propos de l'auteur

Slim OUERTANI est un architecte logiciel avec une expérience dans le monde télécoms et systèmes d’information. Il a participé à la construction et la mise en place de plusieurs solutions notamment au sein de multinationales. Certifié Java, Spring et MongoDB, Slim est passionné par Scala et JEE.

Vous pouvez en savoir plus sur ses récents travaux sur son blog et le suivre sur Twitter : @ouertani.

Bonjour étranger!

Vous devez créer un compte InfoQ ou cliquez sur pour déposer des commentaires. Mais il y a bien d'autres avantages à s'enregistrer.

Tirez le meilleur d'InfoQ

Donnez-nous votre avis

Html autorisé: a,b,br,blockquote,i,li,pre,u,ul,p

M'envoyer un email pour toute réponse à l'un de mes messages dans ce sujet
Commentaires de la Communauté

Html autorisé: a,b,br,blockquote,i,li,pre,u,ul,p

M'envoyer un email pour toute réponse à l'un de mes messages dans ce sujet

Html autorisé: a,b,br,blockquote,i,li,pre,u,ul,p

M'envoyer un email pour toute réponse à l'un de mes messages dans ce sujet

Discuter

Contenu Éducatif

Rien ne serait possible sans le soutien et la confiance de nos Sponsors Fondateurs:

AppDynamics   CloudBees   Microsoft   Zenika
Feedback Général
Bugs
Publicité
Éditorial
InfoQ.com et tous les contenus sont copyright © 2006-2013 C4Media Inc. InfoQ.com est hébergé chez Contegix, le meilleur ISP avec lequel nous ayons travaillé.
Politique de confidentialité
BT