某一天
小明:你好,你是报刊工作人员吗?
报刊:是的,请问,你有什么需要吗?
小明:我想订阅报刊,关于娱乐、经济、社会类的,其它的我不想要,可以吗?
报刊:可以的,并且你可以随时取消订阅。
小明:好的,谢谢你。祝你工作愉快!
通常该过程我们都是在软件上实现的,但是这并不影响我们要说明的内容。实际项目中,业务是复杂多样的,一个业务可能需要依赖另外一个业务,同时也可能取消和该业务的关联,如果我们不做好处理,各业务耦合度将会大大提高,不利于我们扩展和维护。怎样才能解耦这些依赖关系呢?通过上述小故事,大家是不是想到了什么呢,某个设计模式?没错,我们要使用的就是“发布-订阅”模式,使用该模式,订阅者可以订阅自己需要的东西,并且可以随时取消订阅,在这个过程中,发布者不知道订阅者是谁。
事件总线
Akka系统中,是不是需要我们自己去实现“发布-订阅”模式呢?答案肯定是no,Akka系统提供了一种事件总线(Event Bus)工具,内部实现了发布-订阅,通过使用它,我们就可以实现业务之间的解耦。
事件总线,Akka定义为EventBus类型,拥有发布、订阅、取消订阅等功能。处理过程:发布者将Event发布到EventBus上,订阅者将会接受到需要的消息,通常来讲,订阅者是一个Actor,通过createReceive方法接受消息。处理过程中,Classifier被用来描述事件分类,不同的事件有不同的订阅者,当然一个订阅者可以订阅多个事件,EventBus将会通过Classifier来选择订阅者并向其发送消息。常见的Classifier有LookupClassification、SubchannelClassification、ScanningClassification等。
下面我们来看看LookupClassification类型(按照指定的实际类型匹配)的事件分类的EventBus如何实现。
首先定义一个Event实体,拥有事件类型和消息:
@AllArgsConstructor
public class Event {
@Getter
private final String type;
@Getter
private final String message;
}
实体定义使用lombok,一个第三方依赖,可以方便我们开发,减少get、set等重复工作,如果没有使用过,大家可以在网上搜索学习一下,这里不做过多介绍。
继承EventBus,实现发布、订阅、分类等相关方法:
public class MyLookupEventBus extends LookupEventBus<Event,ActorRef,String> {
//期望的classify数,一般为2的n次幂
@Override
public int mapSize() {
return 8;
}
@Override
public int compareSubscribers(ActorRef a1, ActorRef a2) {
return a1.compareTo(a2);
}
@Override
public String classify(Event event) {
return event.getType();
}
@Override
public void publish(Event event, ActorRef actorRef) {
actorRef.tell(event.getMessage(),ActorRef.noSender());
}
}
使用EventBus通常需要三个泛型,分别为指定事件类型、订阅者类型、Classifier类型。当EventBus发布Event,将会通过classify方法来选择目标订阅者,然后使用publish方法通知他们。
定义EventBusActor,作为订阅者:
public class EventBusActor extends AbstractActor {
@Override public Receive createReceive() {
return receiveBuilder().matchAny(other -> {
System.out.println("接受的消息:" + other);
}).build();
} }
下面就使用EventBus.subscribe方法订阅所需事件,如下:
ActorSystem system = ActorSystem.create("system");
ActorRef eventActor = system.actorOf(Props.create(EventBusActor.class), "eventActor");
//定义消息总线
MyLookupEventBus bus = new MyLookupEventBus();
//eventActor订阅add、update消息
bus.subscribe(eventActor, "add");
bus.subscribe(eventActor, "update");
//发布add消息
bus.publish(new Event("add", "insert object"));
//取消订阅
bus.unsubscribe(eventActor, "update");
//发布update消息
bus.publish(new Event("update", "update object"));
测试代码中,eventActor定义了add和update事件,但是它只会收到add事件消息,因为bus在发布update事件消息时,我们已经取消了update事件订阅,所以不会收到update object消息。调用subscribe方法,该方法会在EventBus内部维护一个Map结构,key为classfiy,也就是事件类型,value为订阅者列表,在订阅的过程中,eventBus使用compareSubscribers方法做排序比较。采用事件总线方式,我们可以订阅自己需要的事件,不会收到无关消息,结构清晰,并且可以随时取消订阅,降低代码耦合度。
事件流
事件流是ActorSystem上下文环境中的事件总线,使用它可以很方便的将“发布-订阅模式”用于任意事件类型,可以是自定义类型,也可以是死信(Dead Letter)或者日志(log messages)。订阅者通常都是Actor,当Actor终止时,会自动从订阅者列表移除。
下面我们定义一个订阅死信消息的Actor:
public class DeadLetterActor extends AbstractActor {
@Override
public Receive createReceive() {
return receiveBuilder().match(DeadLetter.class, d -> {
System.out.println("deadLetter:" + d);
}).matchAny(o -> {
System.out.println("其它消息:" + o);
}).build();
}
}
使用事件流非常简单,示例:
ActorSystem system = ActorSystem.create("system");
ActorRef deadLetterActor = system.actorOf(Props.create(DeadLetterActor.class), "deadLetterActor");
system.eventStream().subscribe(deadLetterActor,DeadLetter.class);
DeadLetter.class方式只能订阅常规的死信提醒,DeadLetterSuppression消息除外,如果需要订阅这类死信提醒,我们可以使用
system.eventStream().subscribe(deadLetterActor,DeadLetterSuppression.class);
或
system.eventStream().subscribe(deadLetterActor,AllDeadLetters.class);
AllDeadLetters方式可以获取所有死信提醒消息。
EventStream实现了SubchannelClassification分类器,所以我们可以指定层次事件匹配,订阅一个父类下的所有事件。我们可以定义一个事件接口,定义两个子事件实现它,然后使用EventStream.subscibe定义父事件类型,大家可以自行尝试。
总结
事件总线,基于观察者模式思想,实现了发布-订阅的消息处理,发布者不需要关心订阅者具体是谁,订阅者可以随时取消订阅,在某种程度上降低了系统之间的耦合度,有利于扩展和维护系统。