Integración de akka con spring: akka-java-spring

0
7293

En este tutorial veremos la integración de Spring y Akka, permitiéndonos aplicar a Akka todo el poder del framework de Spring.

Índice de contenidos

1. Introducción

Tras el tutorial de introducción a Akka para java con ejemplos de Javier veremos como somos capaces de delegar la creación de los actores de Akka a Spring de forma que podamos utilizar las características que nos ofrece el framework.
Este tutorial asume que ya conoces los principios básicos de Akka mostrados en el tutorial descrito anteriormente, así que si no conoces Akka te recomiendo que lo leas antes de comenzar :D.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro Retina 17′ (2.93 Ghz Intel Core 2 Duo, 8GB DDR3).
  • Sistema Operativo: Mac OS Yosemite 10.10.3
  • Entorno de desarrollo: IntelliJ IDEA 14

3. Creación del proyecto

Lo primero que necesitamos es crear un nuevo proyecto de maven, en el que tendremos un pom.xml parecido a este:

NOTA: El proyecto está construido con Spring boot dada la sencillez de arranque y configuración que provee

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.autentia</groupId>
    <artifactId>akka-java-spring</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>akka-java-spring</name>
    <description>Proyecto de integracion de akka con spring</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.3.0.M5</version>
        <relativePath/>
        <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.typesafe.akka</groupId>
            <artifactId>akka-actor_2.11</artifactId>
            <version>2.4.0</version>
        </dependency>
        <dependency>
            <groupId>com.typesafe.akka</groupId>
            <artifactId>akka-testkit_2.11</artifactId>
            <version>2.4.0</version>
            <scope>test</scope>
        </dependency>


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>javax.inject</groupId>
            <artifactId>javax.inject</artifactId>
            <version>1</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>akka.repository</id>
            <name>Akka Maven Repository</name>
            <url>http://akka.io/repository</url>
        </repository>

        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </pluginRepository>
        <pluginRepository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </pluginRepository>
    </pluginRepositories>

</project>

Este fichero no tiene ningún misterio, lo más importante es añadir el repositorio de Akka y la dependencia.

4. Comunicación entre el contexto de Spring y el ActorSystem de Akka

En este punto es donde crearemos el «puente de comunicación» entre el contexto de aplicación de Spring y el actor system de Akka, y delegaremos la creación de actores en Spring.

Para ser capaz de utilizar el contexto de aplicación de Spring y dejar que este cree y enlace nuestros actores, necesitamos almacenarlo en un lugar que sea fácilmente accesible para actor system de Akka.
Para esta finalidad Akka nos provee de akka extension, una herramienta que nos permite añadirle características a Akka.
Estas extensiones solo se cargaran una vez por ActorSystem, que será manejado por Akka. La potencialidad de las extensiones de Akka son muy grandes, de hecho muchas características propias de Akka están implementadas usando el sistema de extensiones.

La extensión tiene dos partes principales: la clase SpringExtension que define los métodos usados por Akka para crear la extension para un determinado ActorSystem, y la clase SpringExt que define los métodos y campos disponibles de la extensión.

SpringExtension.java
package org.autentia;

import akka.actor.AbstractExtensionId;
import akka.actor.ExtendedActorSystem;
import akka.actor.Extension;
import akka.actor.Props;
import org.springframework.context.ApplicationContext;

public class SpringExtension extends AbstractExtensionId<SpringExtension.SpringExt> {

    public static SpringExtension SpringExtProvider = new SpringExtension();

    @Override
    public SpringExt createExtension(ExtendedActorSystem system) {
        return new SpringExt();
    }

    public static class SpringExt implements Extension {
        private volatile ApplicationContext applicationContext;

        public void initialize(ApplicationContext applicationContext) {
            this.applicationContext = applicationContext;
        }

        public Props props(String actorBeanName) {
            return Props.create(SpringActorProducer.class, applicationContext, actorBeanName);
        }
    }
}

A continuación procedemos a explicar los aspectos más relevantes de la clase:

  • SpringExtProvider es el id utilizado para acceder a la extensión de Spring.
  • El método createExtension es utillizado internamente por Akka para instanciar la extensión
  • La clase estática SpringExt es la implementación de la extensión que vamos a realizar, y consta de dos métodos:

    • El método initialize es usado para inicializar el contexto de aplicación de Spring en la extensión.
    • El método props crea un Props (la clase encargada de crear la configuración de los actores) para el actor con el nombre del bean pasado por parámetro que se encuentra en el contexto de aplicación que especificamos en el método initialize.
      Para ello utiliza la clase SpringActorProducer, de la que hablaremos a continuación.

A continuación creamos la clase que se va a encargar específicamente de crear el actor:

SpringActorProducer.java
package org.autentia;

import akka.actor.Actor;
import akka.actor.IndirectActorProducer;
import org.springframework.context.ApplicationContext;


public class SpringActorProducer implements IndirectActorProducer {
    private final ApplicationContext applicationContext;
    private final String actorBeanName;

    public SpringActorProducer(ApplicationContext applicationContext, String actorBeanName) {
        this.applicationContext = applicationContext;
        this.actorBeanName = actorBeanName;
    }

    @Override
    public Actor produce() {
        return (Actor) applicationContext.getBean(actorBeanName);
    }

    @Override
    public Class actorClass() {
        return (Class) applicationContext.getType(actorBeanName);
    }
}

Para que SpringActorProducer sea capaz de crear los actores necesita implementar la interfaz IndirectActorProducer, y sobreescribir los dos métodos que tiene.
Observamos como lo que devuelve en el método produce() es el bean de Spring con ese nombre, por lo que ya está Spring encargándose de ese actor, y podremos inyectarle dependencias del resto del proyecto.

El último paso necesario para comunicar Spring y Akka es hacer que el ActorSystem de Akka y el contexto de aplicación de Spring se conozcan. Esto lo logramos por medio de una clase de configuración.

AkkaConfig.java
package org.autentia;

import akka.actor.Actor;
import akka.actor.ActorSystem;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Bean;

@Configuration
public class AkkaConfig {

    @Autowired
    private ApplicationContext applicationContext;

    @Bean
    public ActorSystem actorSystem() {
        ActorSystem actorSystem = ActorSystem.create("ActorSystem");
        SpringExtension.SpringExtProvider.get(actorSystem).initialize(applicationContext);
        return actorSystem;
    }
}

La anotación @Configuration sirve para indicarle a Spring que esta clase es de utilidad a la hora de levantar el contexto, ya que en esta clase somos capaces de inyectar el contexto de aplicación de Spring.
A la hora de necesitar un ActorSystem Akka utilizará el que hemos creado en la clase de configuración, y como no le hemos indicado nada específico su ámbito será de tipo singleton, es decir, que se creará uno solo para toda la aplicación.

5. Ejemplo práctico

Una vez comunicados Spring y Akka vamos a comprobar como cambia la creación de actores y como somos capaces de inyectarles dependencias.
A continuación vamos a comprobar como sería el caso 3 del tutorial realizado por Javier utilizando Spring para que nos maneje los actores.

Comenzamos definiendo cual va a ser el comportamiento general de nuestra aplicación en el método main() de la clase Application.class

Application.java
package org.autentia;

import akka.actor.ActorRef;
import akka.actor.ActorSystem;
import org.autentia.actor.Espadachin;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class Application {
    public static ActorRef espadachin;
    public static ActorRef herrero;
    public static ActorRef minero;

    public static void main(String[] args) {
        final ConfigurableApplicationContext applicationContext = SpringApplication.run(Application.class, args);
        ActorSystem actorSystem = applicationContext.getBean(ActorSystem.class);

        espadachin = actorSystem.actorOf(SpringExtension.SpringExtProvider.get(actorSystem).props("espadachin"), "espadachin");
        herrero = actorSystem.actorOf(SpringExtension.SpringExtProvider.get(actorSystem).props("herrero"), "herrero");
        minero = actorSystem.actorOf(SpringExtension.SpringExtProvider.get(actorSystem).props("minero"), "minero");

        espadachin.tell(Espadachin.Mensaje.ESPADA_ROTA, ActorRef.noSender());
    }
}

Comprobemos los datos más relevantes de esta clase:

  • En la línea 10 vemos la anotación @SpringBootApplication que es la encargada de levantar el contexto de Spring e inicializar los beans de configuración (entre los que se incluye nuestro actorSystem de la clase AkkaConfig).
  • En la línea 18 obtenemos el ActorSystem a través del contexto de aplicación, por lo que seríamos capaces de inyectarlos en cualquier clase. Nosotros preferimos obtenerlo aquí directamente para no complicar el ejemplo.
  • De las líneas 20 a la 22 declaramos a nivel de actorSystem los tres actores del ejemplo. Observar como creamos los actores por medio de la extensión de Akka que hemos creado previamente.
    El parámetro que recibe el método props() es el nombre del bean que va a crear, y el segundo parámetro es el nombre que va a tener el bean respecto al actorPath.
    Soy consciente de que a la hora de crear el actor podríamos añadir la clase SpringExtension como import estático pero he preferido dejarlo así por motivos de claridad a la hora de leer el código.
  • Por último, una vez creado el actor su uso es completamente normal.

Comprobemos ahora las clases de los actores:

Espadachin.java
package org.autentia.actor;

import akka.actor.UntypedActor;
import org.autentia.Application;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Scope;

import javax.inject.Named;

@Named("espadachin")
@Scope("prototype")
public class Espadachin extends UntypedActor {


    public enum Mensaje {
        ESPADA_NUEVA,
        ESPADA_ROTA;
    }

    private static final Logger log = LoggerFactory.getLogger(Espadachin.class);

    @Override
    public void onReceive(Object o) {
        log.info("[Espadachin] ha recibido el mensaje: \"{}\".", o);

        if (o == Mensaje.ESPADA_ROTA) {
            Application.herrero.tell(Herrero.Mensaje.CREAR_ESPADA, getSelf());
        } else if (o == Mensaje.ESPADA_NUEVA) {
            getContext().stop(getSelf());
        } else {
            unhandled(o);
        }
    }
}
Herrero.java
package org.autentia.actor;

import akka.actor.ActorRef;
import akka.actor.UntypedActor;
import org.autentia.Application;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Scope;

import javax.inject.Inject;
import javax.inject.Named;
import java.util.ArrayList;

@Named("herrero")
@Scope("prototype")
public class Herrero extends UntypedActor {
    public enum Mensaje {
        CREAR_ESPADA,
        MATERIALES
    }

    private static final Logger log = LoggerFactory.getLogger(Herrero.class);
    private ArrayList<ActorRef> espadachines;
    private final HerreroService herreroService;

    @Inject
    public Herrero(HerreroService herreroService) {
        this.herreroService = herreroService;
    }

    @Override
    public void preStart() {
        espadachines = new ArrayList<>();
    }

    @Override
    public void onReceive(Object o) throws InterruptedException {
        log.info("[Herrero] ha recibido el mensaje: \"{}\".", o);

        if (o == Mensaje.CREAR_ESPADA) {
            espadachines.add(getSender());
            Application.minero.tell(Minero.Mensaje.OBTENER_MATERIALES, getSelf());
        } else if (o == Mensaje.MATERIALES) {
            log.info("[Herrero] está creando espada...");
            herreroService.crearEspada();
            log.info("[Herrero] ha creado espada.");
            if (!espadachines.isEmpty()) {
                espadachines.get(0).tell(Espadachin.Mensaje.ESPADA_NUEVA, getSelf());
                espadachines.remove(0);
            }
        } else {
            unhandled(o);
        }
    }
}
Minero.java
package org.autentia.actor;

import akka.actor.UntypedActor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Scope;

import javax.inject.Inject;
import javax.inject.Named;

@Named("minero")
@Scope("prototype")
public class Minero extends UntypedActor {

    public enum Mensaje {
        OBTENER_MATERIALES
    }

    private static final Logger log = LoggerFactory.getLogger(Minero.class);
    private final MineroService mineroService;

    @Inject
    public Minero(MineroService service) {
        this.mineroService = service;
    }


    @Override
    public void onReceive(Object o) throws InterruptedException {
        log.info("[Minero] ha recibido el mensaje: \"{}\".", o);

        if (o == Mensaje.OBTENER_MATERIALES) {
            log.info("[Minero] está obteniendo materiales...");
            mineroService.obtenerMinerales();
            log.info("[Minero] ha obtenido materiales.");
            getSender().tell(Herrero.Mensaje.MATERIALES, getSelf());
        } else {
            unhandled(o);
        }
    }
}

Algo muy importante a la hora de crear los actores vía Spring es que hay que anotar la clase con @Named y darle un nombre al actor que sea el mismo que recibe por parámetro el método props().

Como podemos observar en las clases Herrero y Minero tienen dos servicios que se encargan de simular la creación del arma y la obtención de materiales respectivamente. Ambos servicios están inyectados en el constructor vía Spring.

El código de estos servicios es:

HerreroService.java
package org.autentia.actor;

import org.springframework.stereotype.Service;

@Service
public class HerreroService {
    private static final long TIEMPO_CREACION_ESPADA = 2000;

    public void crearEspada() throws InterruptedException {
        Thread.sleep(TIEMPO_CREACION_ESPADA);
    }
}
MineroService.java
package org.autentia.actor;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class MineroService {
    private static final long TIEMPO_OBTENCION_MATERIALES = 2000;
    private static final Logger log = LoggerFactory.getLogger(MineroService.class);

    public void obtenerMinerales() {
        try {
            Thread.sleep(TIEMPO_OBTENCION_MATERIALES);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Para comprobar el proceso de la creación de la espada se usan logs. La salida al ejecutar la aplicación es:

img1

6. Conclusiones

Akka nos ofrece una forma sencilla y amigable de añadir características propias gracias al sistema de extensiones, provocando que sea sencillo utilizar Akka con Spring.

Puedes encontrar el código completo del proyecto aquí.

7. Referencias

  • http://www.typesafe.com/activator/template/akka-java-spring#code/src/main/java/sample/Main.java
  • http://doc.akka.io/docs/akka/2.4.0/java.html?_ga=1.34223657.963062097.1442995264
  • http://doc.akka.io/docs/akka/2.3.4/java/extending-akka.html

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad