我们在前面分析客户端引用的时候会看到如下这段代码:

// 产生开始调用事件
if (EventBus.isEnable(ClientStartInvokeEvent.class)) {
EventBus.post(new ClientStartInvokeEvent(request));
}

这里用EventBus调用了一下post方法之后就什么也没做了,就方法名来看是发送了一个post请求,也不知道发给谁,到底有什么用。

所以这一节我们来分析一下EventBus这个类的作用。

首先我们来看一下这个类的方法

从EventBus的方法中我们是不是应该想到了这是使用了什么设计模式?

没错,这里用到的是订阅发布模式(Subscribe/Publish)。订阅发布模式定义了一种一对多的依赖关系,让多个订阅者对象同时监听某一个主题对象。这个主题对象在自身状态变化时,会通知所有订阅者对象,使它们能够自动更新自己的状态。

我们先分析源码,分析完源码之后再来总结一下。

EventBus发送事件

根据上面的示例,我们先看EventBus#post是里面是怎么做的。

EventBus#post


private final static ConcurrentMap<Class<? extends Event>, CopyOnWriteArraySet<Subscriber>> SUBSCRIBER_MAP = new ConcurrentHashMap<Class<? extends Event>, CopyOnWriteArraySet<Subscriber>>(); public static void post(final Event event) {
//是否开启总线
if (!isEnable()) {
return;
}
//根据传入得event获取到相应的Subscriber
CopyOnWriteArraySet<Subscriber> subscribers = SUBSCRIBER_MAP.get(event.getClass());
if (CommonUtils.isNotEmpty(subscribers)) {
for (final Subscriber subscriber : subscribers) {
//如果事件订阅者是同步的,那么直接调用
if (subscriber.isSync()) {
handleEvent(subscriber, event);
} else { // 异步
final RpcInternalContext context = RpcInternalContext.peekContext();
//使用线程池启动一个线程一部执行任务
final ThreadPoolExecutor asyncThreadPool = AsyncRuntime.getAsyncThreadPool();
try {
asyncThreadPool.execute(
new Runnable() {
@Override
public void run() {
try {
RpcInternalContext.setContext(context);
//调用订阅者的event事件
handleEvent(subscriber, event);
} finally {
RpcInternalContext.removeContext();
}
}
});
} catch (RejectedExecutionException e) {
LOGGER
.warn("This queue is full when post event to async execute, queue size is " +
asyncThreadPool.getQueue().size() +
", please optimize this async thread pool of eventbus.");
}
}
}
}
} private static void handleEvent(final Subscriber subscriber, final Event event) {
try {
subscriber.onEvent(event);
} catch (Throwable e) {
if (LOGGER.isWarnEnabled()) {
LOGGER.warn("Handle " + event.getClass() + " error", e);
}
}
}

这个post方法主要做了这么几件事:

  1. 根据传入的Event获取对应的订阅列表subscribers
  2. 遍历subscribers
  3. 如果订阅者是异步的,那么就使用线程池启动执行任务

    4, 如果是同步的那么就调用handleEvent方法向订阅者发布消息

我们再来看看订阅者是怎样的:

Subscriber

public abstract class Subscriber {
/**
* 接到事件是否同步执行
*/
protected boolean sync = true; /**
* 事件订阅者
*/
protected Subscriber() {
} /**
* 事件订阅者
*
* @param sync 是否同步
*/
protected Subscriber(boolean sync) {
this.sync = sync;
} /**
* 是否同步
*
* @return 是否同步
*/
public boolean isSync() {
return sync;
} /**
* 事件处理,请处理异常
*
* @param event 事件
*/
public abstract void onEvent(Event event); }

Subscriber是一个抽象类,默认是同步的方式进行订阅。总共有下面四个实现类:

LookoutSubscriber

FaultToleranceSubscriber

RestTracerSubscriber

SofaTracerSubscriber

这里我不打算每个都进行分析,到时候打算用到了再详细说明,这样不会那么抽象。

由于我们前面讲到了,在客户端引用的时候会发送一个产生开始调用事件给总线,那一定要有订阅者这个发送事件才有意义。所以我们接下来看看是在哪里进行事件的注册的。

订阅者注册到EventBus

通过上面的继承关系图可以看到,在ConsumerConfig是AbstractIdConfig的子类,所以在初始化ConsumerConfig的时候AbstractIdConfig静态代码块也会被初始化。

public abstract class AbstractIdConfig<S extends AbstractIdConfig> implements Serializable {

    static {
RpcRuntimeContext.now();
}
}

在调用RpcRuntimeContext#now方法的时候,会调用到RpcRuntimeContext的静态代码块

RpcRuntimeContext

public class RpcRuntimeContext {

    static {
if (LOGGER.isInfoEnabled()) {
LOGGER.info("Welcome! Loading SOFA RPC Framework : {}, PID is:{}", Version.BUILD_VERSION, PID);
}
put(RpcConstants.CONFIG_KEY_RPC_VERSION, Version.RPC_VERSION);
// 初始化一些上下文
initContext();
// 初始化其它模块
ModuleFactory.installModules();
// 增加jvm关闭事件
if (RpcConfigs.getOrDefaultValue(RpcOptions.JVM_SHUTDOWN_HOOK, true)) {
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
if (LOGGER.isWarnEnabled()) {
LOGGER.warn("SOFA RPC Framework catch JVM shutdown event, Run shutdown hook now.");
}
destroy(false);
}
}, "SOFA-RPC-ShutdownHook"));
}
} public static long now() {
return System.currentTimeMillis();
}
}

在RpcRuntimeContext静态代码块里主要做了以下几件事:

  1. 初始化一些上下文的东西,例如:应用Id,应用名称,当前所在文件夹地址等
  2. 初始化一些模块,等下分析
  3. 增加jvm关闭时的钩子

我们直接看installModules方法就好了,其他的方法和主流程无关。

ModuleFactory#installModules

public static void installModules() {
//通过SPI加载Module模块
ExtensionLoader<Module> loader = ExtensionLoaderFactory.getExtensionLoader(Module.class);
//moduleLoadList 默认是 *
String moduleLoadList = RpcConfigs.getStringValue(RpcOptions.MODULE_LOAD_LIST);
for (Map.Entry<String, ExtensionClass<Module>> o : loader.getAllExtensions().entrySet()) {
String moduleName = o.getKey();
Module module = o.getValue().getExtInstance();
// judge need load from rpc option
if (needLoad(moduleLoadList, moduleName)) {
// judge need load from implement
if (module.needLoad()) {
if (LOGGER.isInfoEnabled()) {
LOGGER.info("Install Module: {}", moduleName);
}
//安装模板
module.install();
INSTALLED_MODULES.put(moduleName, module);
} else {
if (LOGGER.isInfoEnabled()) {
LOGGER.info("The module " + moduleName + " does not need to be loaded.");
}
}
} else {
if (LOGGER.isInfoEnabled()) {
LOGGER.info("The module " + moduleName + " is not in the module load list.");
}
}
}
}
  1. 这个方法里面一开始获取Module的扩展类,Module的扩展类有如下几个:

    FaultToleranceModule

    LookoutModule

    RestTracerModule

    SofaTracerModule

  2. 然后会去获取MODULE_LOAD_LIST配置类,多个配置用“;”分割。

  3. 调用loader.getAllExtensions()获取所有的扩展类。遍历扩展类。

  4. 接着调用needLoad方法:

static boolean needLoad(String moduleLoadList, String moduleName) {
//用;拆分
String[] activatedModules = StringUtils.splitWithCommaOrSemicolon(moduleLoadList);
boolean match = false;
for (String activatedModule : activatedModules) {
//ALL 就是 *
if (StringUtils.ALL.equals(activatedModule)) {
match = true;
} else if (activatedModule.equals(moduleName)) {
match = true;
} else if (match && (activatedModule.equals("!" + moduleName)
|| activatedModule.equals("-" + moduleName))) {
match = false;
break;
}
}
return match;
}

这个方法会传入配置的moduleLoadList和当前遍历到的moduleName,moduleLoadList默认是*所以会返回true,如果配置了moduleLoadList不为*的话,如果moduleName是配置中的之一便会返回true。

  1. 调用module的install进行模板的装配

这里我们进入到SofaTracerModule#install中

SofaTracerModule#install

public void install() {
Tracer tracer = TracerFactory.getTracer("sofaTracer");
if (tracer != null) {
subscriber = new SofaTracerSubscriber();
EventBus.register(ClientStartInvokeEvent.class, subscriber);
EventBus.register(ClientBeforeSendEvent.class, subscriber);
EventBus.register(ClientAfterSendEvent.class, subscriber);
EventBus.register(ServerReceiveEvent.class, subscriber);
EventBus.register(ServerSendEvent.class, subscriber);
EventBus.register(ServerEndHandleEvent.class, subscriber);
EventBus.register(ClientSyncReceiveEvent.class, subscriber);
EventBus.register(ClientAsyncReceiveEvent.class, subscriber);
EventBus.register(ClientEndInvokeEvent.class, subscriber);
}
}

这里我们可以看到文章一开始被发送的ClientStartInvokeEvent在这里被注册了。订阅者的实现类是SofaTracerSubscriber。

订阅者被调用

在上面我们分析到在注册到EventBus之后,会发送一个post请求,然后EventBus会遍历所有的Subscriber,调用符合条件的Subscriber的onEvent方法。

SofaTracerSubscriber#onEvent

public void onEvent(Event originEvent) {

   if (!Tracers.isEnable()) {
return;
}
Class eventClass = originEvent.getClass(); if (eventClass == ClientStartInvokeEvent.class) {
ClientStartInvokeEvent event = (ClientStartInvokeEvent) originEvent;
Tracers.startRpc(event.getRequest());
} else if (eventClass == ClientBeforeSendEvent.class) {
ClientBeforeSendEvent event = (ClientBeforeSendEvent) originEvent;
Tracers.clientBeforeSend(event.getRequest());
}
.....
}

这个方法里面主要就是对不同的event做出不同的反应。ClientStartInvokeEvent所做的请求就是调用一下Tracers#startRpc,Tracers是用来做链路追踪的,这篇文章不涉及。

总结

我们首先上一张图,来说明一下订阅发布模式整体的结构。

在我们这个例子里EventBus的职责就是调度中心,subscriber的具体实现注册到EventBus中后,会保存到EventBus的SUBSCRIBER_MAP集合中。

发布者在发布消息的时候会调用EventBus的post方法传入一个具体的event来调用订阅者的事件。一个事件有多个订阅者,消息的发布者不会直接的去调用订阅者来发布消息,而是通过EventBus来进行触发。

通过EventBus来触发不同的订阅者的事件可以在触发事件之前同一的为其做一些操作,比如是同步还是异步,要不要过滤部分订阅者等。

SOFARPC源码解析系列:

1. 源码分析---SOFARPC可扩展的机制SPI

2. 源码分析---SOFARPC客户端服务引用

3. 源码分析---SOFARPC客户端服务调用

4. 源码分析---SOFARPC服务端暴露

5.源码分析---SOFARPC调用服务

6.源码分析---和dubbo相比SOFARPC是如何实现负载均衡的?

7.源码分析---SOFARPC是如何实现连接管理与心跳?

8.源码分析---从设计模式中看SOFARPC中的EventBus?的更多相关文章

  1. ABP源码分析十五:ABP中的实用扩展方法

    类名 扩展的类型 方法名 参数 作用 XmlNodeExtensions XmlNode GetAttributeValueOrNull attributeName Gets an   attribu ...

  2. ABP源码分析四十七:ABP中的异常处理

    ABP 中异常处理的思路是很清晰的.一共五种类型的异常类. AbpInitializationException用于封装ABP初始化过程中出现的异常,只要抛出AbpInitializationExce ...

  3. 6.源码分析---和dubbo相比SOFARPC是如何实现负载均衡的?

    官方目前建议使用的负载均衡包括以下几种: random(随机算法) localPref(本地优先算法) roundRobin(轮询算法) consistentHash(一致性hash算法) 所以我们接 ...

  4. Spring源码分析(十三)缓存中获取单例bean

    摘要:本文结合<Spring源码深度解析>来分析Spring 5.0.6版本的源代码.若有描述错误之处,欢迎指正. 介绍过FactoryBean的用法后,我们就可以了解bean加载的过程了 ...

  5. WorkerMan源码分析(resetStd方法,PHP中STDIN, STDOUT, STDERR的重定向)

    WorkerMan中work.php中 resetStd 方法中代码如下 public static function resetStd() { if (!static::$daemonize || ...

  6. jQuery源码分析--为什么在参数列表中传入undefined

    (function(window, undefined){ //jQuery code; })(window); 为什么要传入undefined? 1.没有传入undefined: <!DOCT ...

  7. Netty源码分析之ByteBuf(一)—ByteBuf中API及类型概述

    ByteBuf是Netty中主要的数据容器与操作工具,也是Netty内存管理优化的具体实现,本章我们先从整体上对ByteBuf进行一个概述: AbstractByteBuf是整个ByteBuf的框架类 ...

  8. 9.源码分析---SOFARPC是如何实现故障剔除的?

    SOFARPC源码解析系列: 1. 源码分析---SOFARPC可扩展的机制SPI 2. 源码分析---SOFARPC客户端服务引用 3. 源码分析---SOFARPC客户端服务调用 4. 源码分析- ...

  9. 11.源码分析---SOFARPC数据透传是实现的?

    先把栗子放上,让大家方便测试用: Service端 public static void main(String[] args) { ServerConfig serverConfig = new S ...

随机推荐

  1. redis整合springboot的helloworld

    引入依赖 compile 'org.springframework.boot:spring-boot-starter-data-redis' 使用redis有两种方法 1.Jedis Jedis je ...

  2. 数据结构与算法---树结构(Tree structure)

    为什么需要树这种数据结构 数组存储方式的分析 优点:通过下标方式访问元素,速度快.对于有序数组,还可使用二分查找提高检索速度. 缺点:如果要检索具体某个值,或者插入值(按一定顺序)会整体移动,效率较低 ...

  3. hihoCoder 1308:搜索二·骑士问题(BFS预处理)

    题目链接 题意 中文题意. 思路 对于每一个骑士,可以先预处理出到达地图上某个点的需要走的步数,然后最后暴力枚举地图上每一个点,让三个骑士走过的距离之和最小即可. #include <bits/ ...

  4. django基础知识之后台管理Admin站点:

    Admin站点 通过使用startproject创建的项目模版中,默认Admin被启用 1.创建管理员的用户名和密码 python manage.py createsuperuser 然后按提示填写用 ...

  5. 几款常用的在线API管理工具(是时候抛弃office编写接口文档了)

    在项目开发过程中,总会涉及到接口文档的设计编写,之前使用的都是ms office工具,不够漂亮也不直观,变更频繁的话维护成本也更高,及时性也是大问题.基于这个背景,下面介绍几个常用的API管理工具,方 ...

  6. 宏旺半导体浅谈存储芯片LPDDR4X与UFS2.1的差别

    现在市面上手机参数动不动就是8GB+128GB,手机的这些参数是越大越好吗?这些数字代表什么?宏旺半导体ICMAX给大家科普下. 手机的运行内存RAM——LPDDR4X LPDDR4X为RAM(运存) ...

  7. 《Predict Anchor Links across Social Networks via an Embedding Approach》阅读笔记

    基本信息 文献:Predict Anchor Links across Social Networks via an Embedding Approach 时间:2016 期刊:IJCAI 引言 预测 ...

  8. mac环境下java项目无创建文件的权限

    1.问题: 先抛问题,由于刚刚换用mac环境,之前windows上开发的代码调试完毕,还未上线.之后上线部署之前,tl直连测试本地环境(mac)环境,功能无法使用,显示java.io.IOExcept ...

  9. [转载]linux下清除Squid缓存的方法记录

    在日常运维工作中,只要用到squid缓存服务,就会常常被要求清理squid缓存. 比如公司领导要求删一篇新闻,新闻是生成的静态.运维人员把服务器上静态的新闻页面删除了后,不料代理服务器上缓存还有.缓存 ...

  10. idea万能快捷键(alt enter),你不知道的17个实用技巧!!!

    说明 IDEA里有一个万能快捷键(alt enter),功能非常强大,同一个快捷键,可以根据不同的语境提示你不同的操作, 很多人可能还不了解这些功能,在处理代码的时候还手动处理,了解这些技巧之后,你编 ...