事件总线最初构想作为一种向一组actor发送消息的方式,现在已经被推广为实现简单接口的抽象基类的集合:
/**
* Attempts to register the subscriber to the specified Classifier
* @return true if successful and false if not (because it was already
* subscribed to that Classifier, or otherwise)
*/
public boolean subscribe(Subscriber subscriber, Classifier to);
/**
* Attempts to deregister the subscriber from the specified Classifier
* @return true if successful and false if not (because it wasn't subscribed
* to that Classifier, or otherwise)
*/
public boolean unsubscribe(Subscriber subscriber, Classifier from);
/**
* Attempts to deregister the subscriber from all Classifiers it may be subscribed to
*/
public void unsubscribe(Subscriber subscriber);
/**
* Publishes the specified Event to this bus
*/
public void publish(Event event);
注意
请注意EventBus不会保留被发布消息的发送者。如果你需要原始发送者的引用,那么你不得不在消息内提供。
这个机制用在Akka的不同地方,例如Event Stream。实现可充分利用下面提出的特定的构建块。
事件总线必须定义下面三个类型参数:
· Event (E):总线上发布的所有事件的类型
· Subscriber (S):允许注册到事件总线上的订阅者类型
· Classifier (C):定义分类器,用于分发事件时选择订阅者。
下面的特质仍然是这些类型的泛型,但是任何具体实现都需要需要定义它们。
Classifier
这儿提出的分类器属于Akka发布包的一部分。但是,万一你找不到一个完美的匹配,自定义一个分类器也不是很难,参阅github上的已经存在的实现。
查找分类
最简单的分类就是从每一个事件里提取一个任意的分类器,并为每一个可能的分类器维护一个订阅者集合。这可与收音机的调谐相比较。LookupClassification特质仍然是泛型的,它抽象了如何比较订阅者和如何正确地分类。
要实现的必要方法由下面的例子说明:
publicclass MsgEnvelope {
publicfinal String topic;
publicfinal Object payload;
public MsgEnvelope(String topic, Object payload) {
this.topic = topic;
this.payload = payload;
}
}
import akka.actor.ActorRef;
import akka.event.japi.LookupEventBus;
/**
* Publishes the payload of the MsgEnvelope when the topic of the MsgEnvelope equals the String
* specified when subscribing.
*/
public class LookupBusImpl extends LookupEventBus<MsgEnvelope, ActorRef, String> {
// is used for extracting the classifier from the incoming events
@Override
public String classify(MsgEnvelope event) {
return event.topic;
}
// will be invoked for each event for all subscribers which registered themselves
// for the event’s classifier
@Override
public void publish(MsgEnvelope event, ActorRef subscriber) {
subscriber.tell(event.payload, ActorRef.noSender());
}
// must define a full order over the subscribers, expressed as expected from
// `java.lang.Comparable.compare`
@Override
public int compareSubscribers(ActorRef a, ActorRef b) {
return a.compareTo(b);
}
// determines the initial size of the index data structure
// used internally (i.e. the expected number of different classifiers)
@Override
public int mapSize() {
return 128;
}
}
测试这个实现:
LookupBusImpl lookupBus = new LookupBusImpl();
lookupBus.subscribe(getTestActor(), "greetings");
lookupBus.publish(new MsgEnvelope("time", System.currentTimeMillis()));
lookupBus.publish(new MsgEnvelope("greetings", "hello"));
expectMsgEquals("hello");
如果没有特定事件的订阅者,那么这个分类器是很高效的。
子信道分类
如果分类器形成了一个等级结构,它希望订阅者不只是叶子节点,这种分类器可能是最正确的。它可以与无线电信道(可能有多个)根据类型进行调谐相比。这种分类用于那些分类器只是事件的JVM类,订阅者可能对特定类的所有的子类订阅感兴趣,但是它可用于任何分类器等级。
要实现的必要方法由下面的例子说明:
import akka.util.Subclassification;
class StartsWithSubclassification implements Subclassification<String> {
@Override
public boolean isEqual(String x, String y) {
return x.equals(y);
}
@Override
public boolean isSubclass(String x, String y) {
return x.startsWith(y);
}
}
import akka.actor.ActorRef;
import akka.event.japi.SubchannelEventBus;
import akka.util.Subclassification;
/**
* Publishes the payload of the MsgEnvelope when the topic of the MsgEnvelope starts with the String
* specified when subscribing.
*/
public class SubchannelBusImpl extends SubchannelEventBus<MsgEnvelope, ActorRef, String> {
// Subclassification is an object providing `isEqual` and `isSubclass`
// to be consumed by the other methods of this classifier
@Override
public Subclassification<String> subclassification() {
return new StartsWithSubclassification();
}
// is used for extracting the classifier from the incoming events
@Override
public String classify(MsgEnvelope event) {
return event.topic;
}
// will be invoked for each event for all subscribers which registered themselves
// for the event’s classifier
@Override
public void publish(MsgEnvelope event, ActorRef subscriber) {
subscriber.tell(event.payload, ActorRef.noSender());
}
}
测试这个实现可能看起来是这样的:
SubchannelBusImpl subchannelBus = new SubchannelBusImpl();
subchannelBus.subscribe(getTestActor(), "abc");
subchannelBus.publish(new MsgEnvelope("xyzabc", "x"));
subchannelBus.publish(new MsgEnvelope("bcdef", "b"));
subchannelBus.publish(new MsgEnvelope("abc", "c"));
expectMsgEquals("c");
subchannelBus.publish(new MsgEnvelope("abcdef", "d"));
expectMsgEquals("d");
这个分类器也是在没有任何事件的订阅者时是高效的,但是它使用常规的锁来同步内部的分类器缓存,因此它适用于那些订阅频繁发生变化的场景(记住通过发送第一个消息来打开分类器也必须重新检查所有之前的订阅)。
扫描分类
之前的分类器都是用于多分类器订阅,它们是严格分层的。如果有重叠的分类器都只覆盖了事件空间的一部分,没有形成分层结构,那么扫描分类器就有用了。它可以与无线发射站通过地理的可达性进行调谐相比较(老的无线电波传输)。
要实现的必要方法由下面的例子说明:
import akka.actor.ActorRef;
import akka.event.japi.ScanningEventBus;
/**
* Publishes String messages with length less than or equal to the length specified when
* subscribing.
*/
public class ScanningBusImpl extends ScanningEventBus<String, ActorRef, Integer> {
// is needed for determining matching classifiers and storing them in an
// ordered collection
@Override
public int compareClassifiers(Integer a, Integer b) {
return a.compareTo(b);
}
// is needed for storing subscribers in an ordered collection
@Override
public int compareSubscribers(ActorRef a, ActorRef b) {
return a.compareTo(b);
}
// determines whether a given classifier shall match a given event; it is invoked
// for each subscription for all received events, hence the name of the classifier
@Override
public boolean matches(Integer classifier, String event) {
return event.length() <= classifier;
}
// will be invoked for each event for all subscribers which registered themselves
// for the event’s classifier
@Override
public void publish(String event, ActorRef subscriber) {
subscriber.tell(event, ActorRef.noSender());
}
}
测试这个实现可能看起来是这样的:
ScanningBusImpl scanningBus = new ScanningBusImpl();
scanningBus.subscribe(getTestActor(), 3);
scanningBus.publish("xyzabc");
scanningBus.publish("ab");
expectMsgEquals("ab");
scanningBus.publish("abc");
expectMsgEquals("abc");
这个分类器话费的时间与订阅的数量成正比,而与实际的匹配数量无关。
Actor分类
这个分类器最开始是特地为实现DeathWatch开发的:订阅者和分类器都是ActorRef。
这个分类器需要ActorSystem来执行与成为Actor的订阅者有关的book-keeping操作,这个订阅者可以终止,无需从EventBus取消订阅。ManagedActorClassification维护一个系统Actor,这个系统Actor负责自动取消被终止的actor的订阅。
要实现的必要方法由下面的例子说明:
import akka.actor.ActorRef;
public class Notification {
public final ActorRef ref;
public finalint id;
public Notification(ActorRef ref, intid) {
this.ref = ref;
this.id = id;
}
}
import akka.actor.ActorRef;
import akka.actor.ActorSystem;
import akka.event.japi.ManagedActorEventBus;
public class ActorBusImpl extends ManagedActorEventBus<Notification> {
// the ActorSystem will be used for book-keeping operations, such as subscribers terminating
public ActorBusImpl(ActorSystem system) {
super(system);
}
// is used for extracting the classifier from the incoming events
@Override
public ActorRef classify(Notification event) {
return event.ref;
}
// determines the initial size of the index data structure
// used internally (i.e. the expected number of different classifiers)
@Override
public int mapSize() {
return 128;
}
}
测试这个实现可能看起来是这样的:
ActorSystem system = ActorSystem.create("actor-bus");
ActorRef observer1 = new JavaTestKit(system).getRef();
ActorRef observer2 = new JavaTestKit(system).getRef();
JavaTestKit probe1 = new JavaTestKit(system);
JavaTestKit probe2 = new JavaTestKit(system);
ActorRef subscriber1 = probe1.getRef();
ActorRef subscriber2 = probe2.getRef();
ActorBusImpl actorBus = new ActorBusImpl(system);
actorBus.subscribe(subscriber1, observer1);
actorBus.subscribe(subscriber2, observer1);
actorBus.subscribe(subscriber2, observer2);
Notification n1 = new Notification(observer1, 100);
actorBus.publish(n1);
probe1.expectMsgEquals(n1);
probe2.expectMsgEquals(n1);
Notification n2 = new Notification(observer2, 101);
actorBus.publish(n2);
probe2.expectMsgEquals(n2);
probe1.expectNoMsg(FiniteDuration.create(500, TimeUnit.MILLISECONDS));
system.terminate();
这个分类器的事件类型是个泛型的,对所有场景都是高效的。
Event Stream
事件流是每一个Actor系统的主要事件总线:它用于承载日志消息log messages和死信 Dead Letters,用户代码也可能用于其它的目的。它使用的是Subchannel Classification,能够注册相关的信道集(用于RemotingLifecycleEvent)。下面的例子说明了简单的订阅时如何工作的。考虑一个简单的actor:
import akka.actor.DeadLetter;
import akka.actor.UntypedActor;
public class DeadLetterActor extends UntypedActor {
public void onReceive(Object message) {
if (messageinstanceof DeadLetter) {
System.out.println(message);
}
}
}
可以这样订阅它:
final ActorSystem system = ActorSystem.create("DeadLetters");
final ActorRef actor = system.actorOf(Props.create(DeadLetterActor.class));
system.eventStream().subscribe(actor, DeadLetter.class);
值得指出的是,由于事件流中的子信道分类的实现方式,通过订阅它们的父类有订阅一组事件,如下面的例子所示:
import akka.actor.ActorRef;
import akka.actor.ActorSystem;
import akka.actor.DeadLetter;
import akka.actor.Props;
import akka.actor.UntypedActor;
public class SubcribeAllChannelBySuperType {
static interface AllKindsOfMusic {
}
static class Jazz implements AllKindsOfMusic {
final public String artist;
public Jazz(String artist) {
this.artist = artist;
}
}
static class Electronic implements AllKindsOfMusic {
final public String artist;
public Electronic(String artist) {
this.artist = artist;
}
}
static class Listener extends UntypedActor {
@Override
public void onReceive(Object message) throws Exception {
if (messageinstanceof Jazz) {
System.out.printf("%s is listening to: %s%n", self().path().name(), message);
} else if (message instanceof Electronic) {
System.out.printf("%s is listening to: %s%n", self().path().name(), message);
}
}
}
public static void main(String[] args) {
ActorSystem system = ActorSystem.create("sub");
final ActorRef actor = system.actorOf(Props.create(DeadLetterActor.class));
system.eventStream().subscribe(actor, DeadLetter.class);
final ActorRef jazzListener = system.actorOf(Props.create(Listener.class), "jazz");
final ActorRef musicListener = system.actorOf(Props.create(Listener.class), "allMusic");
system.eventStream().subscribe(jazzListener, Jazz.class);
system.eventStream().subscribe(musicListener, AllKindsOfMusic.class);
// only musicListener gets this message, since it listens to *all* kinds of music:
system.eventStream().publish(new Electronic("Parov Stelar"));
// jazzListener and musicListener will be notified about Jazz:
system.eventStream().publish(new Jazz("Sonny Rollins"));
system.terminate();
}
}
类似于Actor分类,事件流会在它们终止时自动删除订阅者。
注意:事件流是一个本地工具,意思是说它不会分发事件到集群环境中的其它节点(除非你明确地在流里订阅了一个远程的Actor)。如果你需要在Akka集群中广播事件,而且明确地不知道收件人(即获取它们的ActorRefs),你可能想要看看这个集群中分布发布订阅。
默认的处理器
一旦启动,actor system就会创建actor,并订阅事件流中的日志:这些是在application.conf 里配置的处理器,例如:
akka {
loggers = ["akka.event.Logging$DefaultLogger"]
}
这里由全限定类名列出的处理器订阅日志事件类,会比配置的log-level具有更高的高优先级。当在运行时修改log-level时,它们的订阅会保持同步:
system.eventStream.setLogLevel(Logging.DebugLevel());
这意味着某个level的日志事件通常不会被分发(除非手动订阅事件类)
死信Dead Letter
正如Stopping actors介绍的,当actor终止时或者在死亡之后发送的排队消息会被重路由到死信邮箱。死信邮箱默认会发布包装到DeadLetter中的消息。这个包装持有原始的发送者、接收者和重定向的消息信封。
一些内部的消息(标记为DeadLetterSuppression trait)不会像普通消息那样以死信结束。这些消息被设计为安全的,有时候希望到达被终止的actor,因为它们什么都需要担心,它们被默认的死信日志机制压制了。
然而,如果你发现需要调试这些低级别的被压制的死信,仍然可以明确地订阅它们:
system.eventStream().subscribe(actor, SuppressedDeadLetter.class);
或者所有的死信(包含哪些被压制的):
system.eventStream().subscribe(actor, AllDeadLetters.class);
其它用途
事件流总是存在的,并已经准备好被使用,只需要发布你自己的事件 (它接受Object)和用监听器订阅相应的JVM类即可。