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类去确保我们监听了所有的事件。 最后,我们还学习了使用依赖注入来使得我们可以更容易创建基于事件的系统。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/541807.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java PipedOutputStream close()方法与示例

PipedOutputStream类close()方法 (PipedOutputStream Class close() method) close() method is available in java.io package. close()方法在java.io包中可用。 close() method is used to close this PipedOutputStream and free all system resources linked with this str…

Java二维数组谷电,java二维数组遍历的2种代码

二维数组遍历&#xff1a;思想&#xff1a;1.先将二维数组中所有的元素拿到2.再将二维数组中每个元素进行遍历&#xff0c;相当于就是在遍历一个一维数组第一种方法&#xff1a;双重for循环//遍历二维数组public class Traverse_a_two_dimensional_array {public static void m…

【转】MyEclipse快捷键大全

常用快捷键 -------------------------------------MyEclipse 快捷键1(CTRL)-------------------------------------Ctrl1 快速修复CtrlD: 删除当前行 CtrlQ 定位到最后编辑的地方 CtrlL 定位在某行 CtrlO 快速显示 OutLine CtrlT 快速显示当前类的继承结构 CtrlW 关闭当…

Java整数类的compareTo()方法和示例

整数类compareTo()方法 (Integer class compareTo() method) compareTo() method is available in java.lang package. compareTo()方法在java.lang包中可用。 compareTo() method is used to check equality or inequality for this Integer object against the given Integer…

MATLAB元胞自动机报告,元胞自动机概述与MATLAB实现

什么是元胞自动机&#xff1f;元胞自动机(cellular automata&#xff0c;CA) 是一种时间、空间、状态都离散&#xff0c;空间相互作用和时间因果关系为局部的网格动力学模型&#xff0c;具有模拟复杂系统时空演化过程的能力。它能构建随时间推移发生状态转移的系统&#xff0c;…

python(33)多进程和多线程的区别

多线程可以共享全局变量&#xff0c;多进程不能。多线程中&#xff0c;所有子线程的进程号相同&#xff1b;多进程中&#xff0c;不同的子进程进程号不同。 #!/usr/bin/python # -*- coding:utf-8 -*- import os import threading import multiprocessing count_thread 0 coun…

Java FilterInputStream reset()方法与示例

FilterInputStream类的reset()方法 (FilterInputStream Class reset() method) reset() method is available in java.io package. reset()方法在java.io包中可用。 reset() method is used to reset this FilterInputStream to the position set by the most recent call of m…

不同php文件,php-不同文件夹的不同登录(会话)

我有一个Web服务,需要用户登录并创建标准$_SESSION [‘XXX’]个用户变量.我想为应用程序创建一个“演示”,因此为它创建了另一个文件夹.相同的代码在那里,除了数据库以外的所有东西.问题是,当用户登录这两个帐户之一时,它可以访问两个帐户.因此,如果他登录了演示应用程序,它将使…

Java Hashtable containsValue()方法与示例

哈希表类containsValue()方法 (Hashtable Class containsValue() method) containsValue() method is available in java.util package. containsValue()方法在java.util包中可用。 containsValue() method is used to check whether this table Hashtable associated one or m…

php session redis db,php session redis 配置

具体环境&#xff1a;一台apachephp的服务器(yum安装remi源及配置 httpd-2.2.15 php-5.4.45)一台redis服务器(yum安装remi源及配置 redis-3.2.6)保证apache服务器可以访问redis服务器的6379端口具体步骤&#xff1a;1、在apachephp服务器上安装redis扩展点击(此处)折叠或打开yu…

sigprocmask, sigpending, sigsuspend的用法

sigset_t set sigemptyset(&set) :清空阻塞信号集合变量 sigfillset(&set) &#xff1a;添加所有的信号到阻塞集合变量里 sigaddset(&set,SIGINT):添加单一信号到阻塞信号集合变量 sigdelset(&set,SIGINT):从阻塞信号集合变量中删除单一信号 void handler(int …

Java Calendar getDisplayName()方法与示例

日历类的getDisplayName()方法 (Calendar Class getDisplayName() method) getDisplayName() method is available in java.util package. getDisplayName()方法在java.util包中可用。 getDisplayName() method is used to return string denotation of the given calendar fie…

matlab dir数,DIR - matlab函数

DIR List directory.DIR directory_name lists the files in a directory. Pathnames andwildcards may be used. For example, DIR *.m lists all the M-filesin the current directory.D DIR(‘directory_name‘) returns the results in an M-by-1structure with the field…

(四)其他的说明

2019独角兽企业重金招聘Python工程师标准>>> 关于日志&#xff0c;主要是利用aop来实现的。cn.demoframe.test.frame.service.LogAspect&#xff0c;这里在方法前做了个切面setReqReachTime&#xff0c;设置了一个请求达到时间。接下来还有个切面&#xff0c;是在co…

Java LocalDate类| 带示例的compareTo()方法

LocalDate类compareTo()方法 (LocalDate Class compareTo() method) compareTo() method is available in java.time package. compareTo()方法在java.time包中可用。 compareTo() method is used to compare this LocalDate object to the given object. compareTo()方法用于将…

vm中linux物理内存不足解决方案

为什么80%的码农都做不了架构师&#xff1f;>>> 之前创建的一个center os,默认是8GB&#xff0c;经过一顿折磨&#xff0c;装jdk,tomcat,redis,mycat,nginx,mysql,hadoop...终于&#xff0c;内存不足了&#xff0c;在使用docker build某镜像的时候。迭代懵逼了&am…

matlab7.0 6.5,任何处理matlab6.5与7.0.1的兼容问题

mdl文件在6.5里面做的&#xff0c;但是到了7.0里面却打不开&#xff0c;下面就是相关信息&#xff1a;Warning: Unable to load model file d:\MATLAB7\work\*.mdl. Run "bdclose all; set_param(0, CharacterEncoding, Enc)" where Enc is one of windows-1252, I…

Java BigInteger类| 带有示例的减去()方法

BigInteger类减去()方法 (BigInteger Class subtract() method) subtract() method is available in java.math package. exclude()方法在java.math包中可用。 subtract() method is used to subtract the given value from the value of this BigInteger. exclude()方法用于从…

php删除第一个字母,php – 正在上传的文件将第一个字母切断

我正在将网站从具有WS2003,IIS6,PHP 5.2的服务器迁移到具有WS2008,IIS7和PHP 5.3的服务器我有一个html表单,上传文件到网站.if(isset($_POST["Upload"])){echo "";print_r($_POST);print_r($_FILES);echo "";}?>在旧服务器上工作得很好,但在…

.7z.001,.7z.002这样的文件如何解压

1 如图所示&#xff0c;压缩分卷没有显示关联的软件来打开&#xff0c;Winrar右击也无法解压 2 可以使用7-ZIP软件打开该文件&#xff0c;然后选择提取&#xff08;相当于Winrar的解压&#xff09;&#xff0c;然后选择提取路径&#xff0c;默认是同一个文件夹&#xff0c;点击…