Spring非常规集成Akka

最近因为项目中有大量的消息分发需求,突然心血来潮决定挑战一下传说中的并发终极武器AKKA。

因为这个项目是spring boot项目,所以加入Scala一点问题没有。

这里去除了项目中的业务代码,只保留最基本的系统结构。

关于如何搭建spring + Scala的混编环境,不论采用Maven或Gradle作为构建工具网上都有大量的文章,也可以参考我的另一篇博客,java与scala混合编程打包(maven构建)。一直没有机会接触纯Scala项目,所以SBT嘛......不会!

AKKA中,Actor对象的管理是由ActorSystem进行管理。可以简单的把它理解为一个容器,提供Actor对象的管理。那么第一步就是在合适的场景中初始化该容器。该容器的作用其实就是创建第一个Actor对象。

import akka.actor.ActorSystem
import akka.actor.Props
import akka.routing.RandomPool
import org.springframework.context.annotation.Bean
import org.springframework.stereotype.Component

@Component
class AkkaConfig {
  
  val system = ActorSystem("ReactiveEnterprise")
  
  val processManagersRef = system.actorOf(Props[ProcessManagers].withDispatcher("my-thread-pool-dispatcher").withRouter(RandomPool(2)),"processManagers")
  
  @Bean
  def processManagers = {
    processManagersRef
  }
}

自定义一个类,将ActorSystem容易作为该类的属性,并对外暴露,同时创建ActorRef对象,并作为一个Spring bean托管于Spring IOC容器。

现在ActorRef对象就是一个Spring IOC容中的对象,注入到依赖方即可。

import org.springframework.stereotype.Service
import org.springframework.beans.factory.annotation.Autowired
import akka.actor.ActorRef
import scala.concurrent.Await
import akka.pattern.ask
import akka.util.Timeout
import java.util.concurrent.TimeUnit
import scala.concurrent.Future
import org.slf4j.LoggerFactory
import com.sam.demo.akka.scala.Message.TextMessage

@Service
class UserService {

  @Autowired var processManagers: ActorRef = _
  
  implicit val timeout = Timeout(60, TimeUnit.SECONDS)
  
  val logger = LoggerFactory.getLogger(getClass)
  
  def send(x: String) = {
    logger.info("{}", x);
    val result = Await.result(ask(processManagers, x).mapTo[TextMessage], timeout.duration)
    val resultMap = scala.collection.immutable.Map("value" -> result.msg)
    resultMap
  }
}

现在processManagers已经被注入到UserService,注意:UserService并不是ActorSystem容器中的对象。

真实向Actor发消息的地址是val result = Await.result(ask(processManagers, x).mapTo[TextMessage], timeout.duration)。简单的发了一个String类型的参数。

如果不关心Actor对象的返回值,则可以更简单的通过processManagers ! x发消息即可。

在Actor ProcessManagers中,当然是通过receive函数接收消息

import akka.actor.Actor
import akka.actor.Props
import akka.routing.RandomPool
import org.slf4j.LoggerFactory
import akka.actor.ActorRef
import akka.pattern.ask
import akka.util.Timeout
import java.util.concurrent.TimeUnit
import scala.concurrent.Await
import scala.concurrent.Future

class ProcessManagers extends Actor {
  
  val logger = LoggerFactory.getLogger(getClass)

  val worker = context.actorOf(Props[Worker].withDispatcher("my-thread-pool-dispatcher").withRouter(RandomPool(4)), "worker")
  
  implicit val timeout = Timeout(60, TimeUnit.SECONDS)
  
  def receive = {
    case x: String => 
      logger.info("{}", x);
      sender ! Await.result(ask(worker, x), timeout.duration)
    case x: Int => 
      logger.info("{}", x)
  }
}

接收到消息后,将该消息再发送给另一个Actor对象,worker。Worker由ProcessManagers在初始化时通过context创建。context是从父类Actor继承而来的ActorContext(不同于ActorSystem)。Worker是ProcessManagers的下级Actor,受ProcessManagers的监控。

import akka.actor.Actor
import org.slf4j.LoggerFactory
import com.sam.demo.BeanFactory
import com.sam.demo.web.controller.DemainService

class Worker extends Actor {
  
  val logger = LoggerFactory.getLogger(getClass)
  
  lazy val demainService = BeanFactory.buildFactory().getBean(classOf[DemainService])
  
  def receive = {
    case x: String => {
      logger.info("{}", x);
      sender ! new Message.TextMessage(demainService.handler)
    }
  }
}

重点来了,在Worker接收到消息后,需要对消息进行业务处理,这时候往往需要调用传统的Spring IOC容器中的服务对象。但Worker并不受Spring IOC容器的管理,所以自然没法通过Spring IOC进行注入。既然不能通过Spring注入,那么换条路,从Spring BeanFactory中getBean即可。所以lazy val demainService = BeanFactory.buildFactory().getBean(classOf[DemainService])就顺理成章了。

我这里的BeanFactory是自定义的一个Java类。

import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class BeanFactory implements ApplicationContextAware {

    private ApplicationContext applicationContext;
    private static BeanFactory factory;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
        factory = this;
    }
    
    public <T> T getBean(Class<T> clazz) {
        return (T) applicationContext.getBean(clazz);
    }
    
    public Object getBean(String name) {
        return applicationContext.getBean(name);
    }
    
    public static BeanFactory buildFactory(){
        return factory;
    }
    
}

实现ApplicationContextAware,同时提供通用的getBean方法,这里的getBean方法只是简单的从Spring ApplicationContext中获取对象并返回。

object Message {
  case class TextMessage(msg: String)
}

回过头看Worker,通过demainService.handler模拟业务方法的调用,并封装为另一个消息对象Message(简单的一个case class)。并通过sender方法将Message消息传递给发送者ProcessManagers

而ProcessManagers在接收到Message消息后再发送给自己的sender(UserService)。

UserService中通过mapTo[TextMessage]将消息转型并得到Message中的msg值,并指定超时时间(timeout)。 至此完成整个调用过程。

绕这么大一圈,好处是什么呢?异步。

UserService-->ProcessManagers-->Worker,

Worker-->ProcessManagers-->UserService。整个调用链和返回链都是不同的线程执行。

通过输出日志可以看到UserService,ProcessManagers,Worker分别由不同的线程执行。避免了手工管理线程池。简化了线程间调用模型。当然,线程安全的问题同样存在,依然需要小心翼翼。

00:51:57 [http-nio-8080-exec-1] INFO  com.sam.demo.akka.scala.UserService.send - hello
00:51:57 [ReactiveEnterprise-my-thread-pool-dispatcher-19] INFO  com.sam.demo.akka.scala.ProcessManagers$$anonfun$receive$1.applyOrElse - hello
00:51:57 [ReactiveEnterprise-my-thread-pool-dispatcher-20] INFO  com.sam.demo.akka.scala.Worker$$anonfun$receive$1.applyOrElse - hello

最后加上application.conf

akka {
    loggers = ["akka.event.slf4j.Slf4jLogger"]
    loglevel = "DEBUG"
    stdout-loglevel = "DEBUG"
    logging-filter = "akka.event.slf4j.Slf4jLoggingFilter"
    actor {
        # 可选
        # akka.cluster.ClusterActorRefProvider
        # akka.remote.RemoteActorRefProvider
        # akka.actor.LocalActorRefProvider
        
        provider = "akka.actor.LocalActorRefProvider"
        default-dispatcher {
            throughput = 2
        }   
    }
}

my-thread-pool-dispatcher {
  # Dispatcher是基于事件的派发器的名称
  type = Dispatcher
  # 使用何种 ExecutionService
  executor = "thread-pool-executor"
  # 配置线程池
  thread-pool-executor {
    # 容纳基于因子的内核数的线程数下限
    core-pool-size-min = 1
    # 内核线程数 .. ceil(可用CPU数*倍数)
    core-pool-size-factor = 2.0
    # 容纳基于倍数的并行数量的线程数上限
    core-pool-size-max = 200
  }
  # Throughput 定义了线程切换到下一个actor之前处理的消息数上限
  # 设置成1表示尽可能公平.
  throughput = 1
}

这里用到的都是LocalActorRefProvider,Akka更强大的地方是RemoteActorRefProvider和ClusterActorRefProvider。后续有机会再补充。

Akka本身是用Scala开发的,也提供Java客户端,不过个人建议还是使用Scala进行Akka开发,至少语法上要简洁很多。

补充:akka的maven依赖

    <dependency>
        <groupId>org.scala-lang</groupId>
        <artifactId>scala-library</artifactId>
        <version>2.11.8</version>
    </dependency>

    <dependency>
        <groupId>org.scala-lang</groupId>
        <artifactId>scala-compiler</artifactId>
        <version>2.11.8</version>
    </dependency>

    <dependency>
        <groupId>com.typesafe.akka</groupId>
        <artifactId>akka-actor_2.11</artifactId>
        <version>2.4.17</version>
    </dependency>

    <dependency>
        <groupId>com.typesafe.akka</groupId>
        <artifactId>akka-slf4j_2.11</artifactId>
        <version>2.4.17</version>
    </dependency>

    <dependency>
        <groupId>com.typesafe.akka</groupId>
        <artifactId>akka-cluster_2.11</artifactId>
        <version>2.4.17</version>
    </dependency>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • 背景 我们所有产品在初期的时候都使用的Java语言作为后端开发语言,整个架构在演进了几次之后形成了基于微服务的一个...
    墨弈阅读 2,533评论 1 49
  • 在 之前的文章 中介绍了spring boot简化集成Akka,最近通过在项目中的实践,又有的新的想法。 首先定义...
    SamHxm阅读 5,508评论 2 4
  • 1.帮助 我帮助你,是我的责任,但并不是我的义务。 我不愿帮助一个不懂得感恩的人。 恩,这种人我连理都不想理。 生...
    是现实还是梦阅读 433评论 0 2
  • 如果有一天,我们淹没在茫茫人海中,庸碌一生,那一定是我们没有努力活得丰盛.。 小说,与电影一般,饱含戏剧张力。本书...
    松子糖balabala阅读 367评论 4 1