Guava翻译系列之EventBus

EventBus 类解析

当我们开发软件时,各个对象之间的数据共享和合作是必须的。 但是这里比较难做的是 怎样保证消息之间的传输高效并且减少各个模块之间的耦合。 当组件的职责不清楚时,一个组件还要承担另一个组件的职责,这样的系统我们就认为是高耦合。 当我们的系统变得高耦合时,任何一个小的改动都会对系统造成影响。 为了解决设计上的问题,我们设计了基于事件的设计模型。 在事件驱动编程模型中,对象可以发布/订阅 事件. 事件监听者就是监听事件的发生,我们在第六章中已经看到过RemovalListener, 在这一章中,我们将讨论Guava的EventBus类,了解它的发布/订阅事件是怎么使用的。

这一章,我们将覆盖下面的知识点:
-- EventBus 和 AsyncEventBus类
-- 怎样是用EventBus订阅事件
-- 使用EventBus发布事件
-- 编写事件处理器,并且根据我们的需求选择合适的处理器
-- 与DI工具协作

EventBus

EventBus类是guava中关注消息的发布和订阅的类,简单的说订阅者通过EventBus注册并订阅事件,发布者将事件发送到EventBus中,EventBus将事件顺序的通知给时间订阅者,所以 这里面有一个重要的注意点,事件处理器必须迅速的处理,否则可能会导致时间堆积。

创建EventBus实例

创建一个EventBus实例,只需要简单的调用构造方法:

EventBus eventBus = new EventBus();

也提供了一个带参数的构造类,目的只是为了加上一个标识:

EventBus eventBus = new EventBus(TradeAccountEvent.class.getName());

订阅事件

为了接受到一个事件,我们需要做下面3个步骤:
1. 这个类需要定义一个只接受一个参数的public方法, 参数的类型要和订阅的事件类型一只。
2. 需要在方法上加上@Subscribe注解
3. 最后我们调用EventBus的register方法注册对象

发布事件

我们可以调用EventBus.post方法发送事件,EventBus会轮流调用所有的接受类型是发送事件类型的订阅者,但是这里面有一个比较强大的东西,就是。。。。。。。。。。。

定义事件处理方法

如前面提到了事件处理方法只能接受一个参数,EventBus会轮流顺序调用订阅的方法,因此事件处理方法必须很快的给予响应,如果说时间处理的方法中有需要进行长时间运算的过程,我们建议另起一个线程处理。

并发

EventBus不会起多个线程去调用时间处理方法,除非我们在事件的处理方法上加上注解@AllowCOncurrentEvent,加上这个注解后我们就会认为这个事件处理方法是线程安全的.Annotating a handler method with the @
AllowConcurrentEvent annotation by itself will not register a method with EventBus

现在我们来看看怎么使用EventBus,我们来看一些例子.

订阅事件

我们假设我们已经像如下的方式定义了一个事件:

public class TradeAccountEvent {
private double amount;
private Date tradeExecutionTime;
private TradeType tradeType;
private TradeAccount tradeAccount;
public TradeAccountEvent(TradeAccount account, double amount,
Date tradeExecutionTime, TradeType tradeType) {
checkArgument(amount > 0.0, "Trade can't be less than
zero");
this.amount = amount;
this.tradeExecutionTime =
checkNotNull(tradeExecutionTime,"ExecutionTime can't be null");
this.tradeAccount = checkNotNull(account,"Account can't be
null");
this.tradeType = checkNotNull(tradeType,"TradeType can't
be null");
}

由上面可以看出,无论是买或者卖的事件发生,我们都会创建一个TradeAccountEvent对象,现在让我们考虑一下我们当这个事件被执行时我们希望监听者能够接收到,我们定义SimpleTradeAuditor类:

public class SimpleTradeAuditor {
private List<TradeAccountEvent> tradeEvents =
Lists.newArrayList();
public SimpleTradeAuditor(EventBus eventBus){
eventBus.register(this);
}
@Subscribe
public void auditTrade(TradeAccountEvent tradeAccountEvent){
tradeEvents.add(tradeAccountEvent);
System.out.println("Received trade "+tradeAccountEvent);
}
}

我们可以快速的看一下代码,在构造方法中,我们接受一个EventBus实例,接着我们注册SimpleTradeAuditor类到EventBus中,接受事件TradeAccountEvents. 通过指定@Subscribe注解说明哪个方法是事件处理器. 上面例子中的处理方式:将event加入到list中,并且在控制台中打印出来.

事件发布 例子

现在我们看一下怎样发布一个事件,看下面的类:

public class SimpleTradeExecutor {
private EventBus eventBus;
public SimpleTradeExecutor(EventBus eventBus) {
this.eventBus = eventBus;
}
public void executeTrade(TradeAccount tradeAccount, double
amount, TradeType tradeType){
TradeAccountEvent tradeAccountEvent =
processTrade(tradeAccount, amount, tradeType);
eventBus.post(tradeAccountEvent);
}
private TradeAccountEvent processTrade(TradeAccount
tradeAccount, double amount, TradeType tradeType){
Date executionTime = new Date();
String message = String.format("Processed trade for %s of
amount %n type %s @
%s",tradeAccount,amount,tradeType,executionTime);
TradeAccountEvent tradeAccountEvent = new TradeAccountEvent(tr
adeAccount,amount,executionTime,tradeType);
System.out.println(message);
return tradeAccountEvent;
}
}

像上面的SimpleTradeAuditor类一样,在SimpleTradeExecutor的构造方法中我们也接受一个EventBus作为构造参数. 和 SimpleTradeAuditor类,为了方便后面使用,我们使用了一个成员变量引用了eventbus类,尽管大多数情况下,在两个类中使用同一个eventBus实例是不好的,我们将在后面的例子中去看怎样使用多个EventBus实例。 但是在这个例子中,我们使用同一个实例. SimpleTradeExecutor类有一个公开的方法,executeTrade接受我们处理一个trade的所有信息。 在这个例子中我们调用processTrade方法,传入了必要的信心并且在控制台中打印了交易已经被执行,并且返回一个TradeAccountEvent实例。 当processTrade 方法执行完,我们调用EventBus的post方法将TradeAccountEvent作为参数, 这样所有订阅TradeAccountEvent事件的订阅者都会收到这个消息。 这样我们就可以看到,publish 类和 scribe类通过消息解耦了

精确订阅

我们刚才了解了怎样使用EventBus订阅发布事件。 我们知道 EventBus事件的发布与订阅是基于事件类型的, 这样我们就可以通过事件类型将事件发送给不同的订阅者。 比如: 如果我们我们想分别订阅 买和卖事件。 首先我们创建两种类型的事件:

public class SellEvent extends TradeAccountEvent {
public SellEvent(TradeAccount tradeAccount, double amount, Date
tradExecutionTime) {
super(tradeAccount, amount, tradExecutionTime, TradeType.
SELL);
}
}
public class BuyEvent extends TradeAccountEvent {
public BuyEvent(TradeAccount tradeAccount, double amount, Date
tradExecutionTime) {
super(tradeAccount, amount, tradExecutionTime, TradeType.BUY);
}
}

现在我们创建了两种不同类型的事件实例,SellEvent和BuyEvent,两个事件都继承了TradeAccountEvent。 我们能够实现分别的订阅,我们先创建一个能够订阅SellEvent的实例:

public class TradeSellAuditor {
private List<SellEvent> sellEvents = Lists.newArrayList();
public TradeSellAuditor(EventBus eventBus) {
eventBus.register(this);
}
@Subscribe
public void auditSell(SellEvent sellEvent){
sellEvents.add(sellEvent);
System.out.println("Received SellEvent "+sellEvent);
}
public List<SellEvent> getSellEvents() {
return sellEvents;
}
}

从功能点上来看,这个实例和我们之前的SimpleTradeAuditor差不多,只不过上面的这个实例只接受SellEvent事件,下面我们再创建一个只接受BuyEvent事件的实例:

public class TradeBuyAuditor {
private List<BuyEvent> buyEvents = Lists.newArrayList();
public TradeBuyAuditor(EventBus eventBus) {
eventBus.register(this);
}
@Subscribe
public void auditBuy(BuyEvent buyEvent){
buyEvents.add(buyEvent);
System.out.println("Received TradeBuyEvent "+buyEvent);
}
public List<BuyEvent> getBuyEvents() {
return buyEvents;
}
}

现在我们只需要重构我们的SimpleTradeExecutor类去基于buy或者sell创建正确的TradeAccountEvent。

public class BuySellTradeExecutor {
… deatails left out for clarity same as SimpleTradeExecutor
//The executeTrade() method is unchanged from SimpleTradeExecutor
private TradeAccountEvent processTrade(TradeAccount tradeAccount,
double amount, TradeType tradeType) {
Date executionTime = new Date();
String message = String.format("Processed trade for %s of
amount %n type %s @ %s", tradeAccount, amount, tradeType,
executionTime);
TradeAccountEvent tradeAccountEvent;
if (tradeType.equals(TradeType.BUY)) {
tradeAccountEvent = new BuyEvent(tradeAccount, amount,
executionTime);
} else {
tradeAccountEvent = new SellEvent(tradeAccount,
amount, executionTime);
}
System.out.println(message);
return tradeAccountEvent;
}
}

这里我们创建了和SimpleTradeExecutor功能相似的类:BuySellTradeExecutor,只不过BuySellTradeExecutor根据交易类型创建了不同的事件,BuyEvent和SellEvent。 我们发布了不同的事件,注册了不通的订阅者,但是EventBus对这样的改变没有感知。 为了接受这两个事件,我们不需要创建两个类,我们只需要像如下这种方式就可以:

public class AllTradesAuditor {
private List<BuyEvent> buyEvents = Lists.newArrayList();
private List<SellEvent> sellEvents = Lists.newArrayList();

public AllTradesAuditor(EventBus eventBus) {
eventBus.register(this);
}
@Subscribe
public void auditSell(SellEvent sellEvent){
sellEvents.add(sellEvent);
System.out.println("Received TradeSellEvent "+sellEvent);
}
@Subscribe
public void auditBuy(BuyEvent buyEvent){
buyEvents.add(buyEvent);
System.out.println("Received TradeBuyEvent "+buyEvent);
}
}

上面我们创建一个实例,有两个事件处理方法,这样AllTradeAuditor会接受所有的Trade事件。 哪个方法被调用取决于EventBus发送什么类型的事件。 为了验证一下,我们可以写一个方法接受Object类型的参数,这样就可以收到所有的事件。
下面我们来考虑一下我们有多个EventBus实例。 如果我们把BuySellTradeExecutor类拆分成两个类,这样我们就可以注入两个不同的EventBus实例,但是在订阅类中就要注意注入的是哪个类了。 关于这个例子我们在这里不讨论了,代码可以见

bbejeck.guava.chapter7.config 包。

取消事件订阅

我们订阅事件的时候,肯定也会想到在某个时间点我们需要取消订阅某个事件。 取消事件订阅只需要调用eventBus.unregister方法。 如果我们知道我们在某个时刻想要停止对某个事件的处理,我们可以按照如下的方式处理:

public void unregister(){
this.eventBus.unregister(this);
}

一旦上面的方法被调用,就不在会收到任何事件,其他的没有取消订阅的会继续收到事件.

异步事件总线

我们之前一直强调事件处理器的处理逻辑要简单。 因为EventBus是顺序的处理每一个事件的。 我们还有另外一种处理方式: AsyncEventBus. AsyncEventBus提供了和EventBus相同的功能。只是在处理事件的时候采用了Java.util.concurrent.Executor 调用事件处理器。

创建一个异步EventBus实例

创建AsyncEvent和创建一个EventBus差不多:

AsyncEventBus asyncEventBus = new AsyncEventBus(executorService);

我们创建一个传入ExecutorService实例的AsyncEvent实例。 我们还有一个接受两个参数的构造函数,接受另外一个string表明ExecutorService的身份。 AysncEventBus在事件处理器需要花费时间比较长的场景下比较适合。

DeadEvent

当一个事件没有监听者,我们就会将这样的事件包装成DeadEvent,这样有一个方法监听DeadEvent我们就可以知道哪些事件没有监听者。

public class DeadEventSubscriber {
private static final Logger logger =
Logger.getLogger(DeadEventSubscriber.class);
public DeadEventSubscriber(EventBus eventBus) {
eventBus.register(this);
}
@Subscribe
public void handleUnsubscribedEvent(DeadEvent deadEvent){
logger.warn("No subscribers for "+deadEvent.getEvent());
}
}

上面的例子中我们简单的注册了一个监听DeadEvent的监听者,简单的记录了没有被监听的事件。

依赖注入

为了确保我们注册的监听者和发布者是同一个EventBus实例,我们使用Spring来实现EventBus的注入。 在下面的例子中,我们将展示怎样使用Spring框架去配置SimpleTradeAuditor 和 SimpleTradeExecutor类。首先我们需要对SimpleTradeAuditor和SimpleTradeExecutor类做如下的改变:

@Component
public class SimpleTradeExecutor {
private EventBus eventBus;
@Autowired
public SimpleTradeExecutor(EventBus eventBus) {
this.eventBus = checkNotNull(eventBus, "EventBus can't be
null");
}

@Component
public class SimpleTradeAuditor {
private List<TradeAccountEvent> tradeEvents =
Lists.newArrayList();
@Autowired
public SimpleTradeAuditor(EventBus eventBus){
checkNotNull(eventBus,"EventBus can't be null");
eventBus.register(this);
}

我们首先在两个类上加上了@Component注解。 这样可以让Spring把这两个类当作可以注入的bean。这里我们使用的构造方法注入。所以我们加上了@Autowired注解。 加上了@Autowired注解spring就可以帮助我们注入EventBus类。

@Configuration
@ComponentScan(basePackages = {"bbejeck.guava.chapter7.publisher",
"bbejeck.guava.chapter7.subscriber"})
public class EventBusConfig {
@Bean
public EventBus eventBus() {
return new EventBus();
}
}

这里我们的类上加上了@Configuration注解,这样spring就把这个类当作Context,在这个类中我们返回了EventBus实例。这样spring就对上面的两个类注入了同一个EventBus,这正是我们想要的,至于spring是怎么做到的,不在本书考虑的范围。

总结

在这一章,我们讲解了怎样使用Guava进行事件驱动编程,来降低模块之间的耦合,我们讲解了怎么创建EventBus实例,并且怎样注册监听者和发布者。 并且我们也剖析了怎样更具事件类型去监听事件。 最后我们还学习了使用AsyncEventBus类,可以让我们异步的发送事件。我们也学习了怎样使用DeadEvent类去确保我们监听了所有的事件。 最后,我们还学习了使用依赖注入来使得我们可以更容易创建基于事件的系统。

时间: 2024-09-11 03:44:22

Guava翻译系列之EventBus的相关文章

guava翻译系列之Cache

Guava Cache 在软件开发的过程,缓存是一个非常重要的话题. 在稍微复杂的开发过程中,我们基本上是不可能不使用到缓存的. 至少我们会使用Map去存储一些东西. 这其实就是一个最简单的缓存. Guava给我们提供了比简单的使用HashMap更强大更灵活的功能,但是和专业的缓存工具相比,(EHCache,Memcached)功能还有些不足, 那么这一章,我们将覆盖Guava cache的下面几个方面: -- 使用MapMaker类创建ConcurrentMap实例 -- 使用CacheBui

Guava翻译系列之File

使用guava处理文件 读写文件是一个程序员的核心能力! 令人意外的事,虽然java有非常丰富的并且强壮的I/O接口,但是却不怎么好用. 虽然在java7中已经有了一些改善. 但是我们还是要学一下guava的I/O相关的工具. 这一章我们要学习一下内容: -- 使用Files类处理文件的移动和复制,或者从文件中读取内容到字符串中 -- Closer 类 给我们提供非常简洁干净的方式去确保文件被正确关闭 -- ByteSource 和 CharSource 类,是inputStream和reade

guava翻译系列之Joiner

基本的guava工具 在前面的章节,我们已经讨论了什么是GUAVA和怎样去安装GUAVA,在这一章我们将开始使用guava,我们将展示guava提供的基本功能,并且了解一下这些基本功能是怎样帮助我们简化日常工作遇到的的问题 在这一章节中,我们将覆盖一下几个方面的内容: 使用Joiner Class 将字符串以指定的分隔符连接起来. 我们也会涉及到使用MapJoiner Splitter Class,和Joiner的作用相反,将一个字符串以给定的分隔符分隔开 使用CharMatcher 和 Str

guava翻译系列之Collections

引言 集合对于任何一门语言都是必须的.没有集合我们写不出一些复杂的逻辑.Guava继承扩展了Google Collections的一些功能. 从 com.google.common.collect包下面的类的数量,我们就可以看出Collections的重要性. 虽然已经有了这么多的工具类,但是还是有很多场景我们没有覆盖到,我们希望我们能够覆盖到日常使用的哪些. 下面我们就在每天的编程中经常会使用的类做一下介绍. 这一章节中我们将涉及以下几个方面: lists,maps,sets 等这些包含非常有

guava翻译系列之Splitter

使用Splitter类 另一个常见的操作就是解析一个以固定分隔符分隔的字符串,并返回一个包含这个String的数组,如果你需要去阅读一个text文件,你会经常要处理这种情况.但是String.split方法还有一些可以改进的地方,就像下面的例子所展示的:String testString = "Monday,Tuesday,,Thursday,Friday,,"; //parts is [Monday, Tuesday, , Thursday,Friday] String[] part

Guava学习笔记:EventBus(转)

EventBus是Guava的事件处理机制,是设计模式中的观察者模式(生产/消费者编程模型)的优雅实现.对于事件监听和发布订阅模式,EventBus是一个非常优雅和简单解决方案,我们不用创建复杂的类和接口层次结构. Observer模式是比较常用的设计模式之一,虽然有时候在具体代码里,它不一定叫这个名字,比如改头换面叫个Listener,但模式就是这个模式.手工实现一个Observer也不是多复杂的一件事,只是因为这个设计模式实在太常用了,Java就把它放到了JDK里面:Observable和O

Oracle ASM 翻译系列第八弹:ASM Internal ASM file extent map

当ASM创建一个文件时(例如数据库实例要求创建一个数据文件),它会以extent为单位分配空间.一旦文件被创建,ASM会传递extent映射表给数据库实例,后续数据库实例能在不和ASM实例交互的情况下访问这个文件.如果一个文件的extent需要被重新定位,比如磁盘组进行rebalance操作,ASM会告知数据库实例关于extent映射表的变更. 可以通过查询ASM实例的X$KFFXP视图来获取ASM文件extent映射表的内容.X$KFFXP视图中的每一行对应着所有处于mount状态磁盘组中每一

7.Swift教程翻译系列——控制流之循环

英文版PDF下载地址http://download.csdn.net/detail/tsingheng/7480427 Swift提供了类C语言类似的控制流结构.包括for循环和while循环来多次执行任务,if和switch语句根据不同的条件执行不同的分支代码,break和continue语句将执行流程跳转到其他语句. 除了C里面传统的for-条件-递增循环,Swift还增加了for-in循环使得遍历数组,字典,范围,字符串或者其他序列都很简单. Swift的switch语句也要比C语言的sw

ASM 翻译系列第三十二弹:自制数据抽取小工具

Find block in ASM 在本系列文章[ Where is my data]中,我已经演示了如何从ASM磁盘中定位和抽取一个Oracle的block,为了让这件事做起来不那么复杂,我又写了一个perl脚本find_block.pl来简化整个操作,只需要提供数据文件的名称和需要提取的block,这个脚本就可以输出从ASM磁盘组中抽取块的命令. find_block.pl find_block.pl是一个perl脚本,脚本里集成了dd或kfed命令来从ASM磁盘中抽取一个块,脚本可以在Li