8.源码分析---从设计模式中看SOFARPC中的EventBus?
我们在前面分析客户端引用的时候会看到如下这段代码:
// 产生开始调用事件
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方法主要做了这么几件事:
- 根据传入的Event获取对应的订阅列表subscribers
- 遍历subscribers
- 如果订阅者是异步的,那么就使用线程池启动执行任务
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静态代码块里主要做了以下几件事:
- 初始化一些上下文的东西,例如:应用Id,应用名称,当前所在文件夹地址等
- 初始化一些模块,等下分析
- 增加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.");
}
}
}
}
这个方法里面一开始获取Module的扩展类,Module的扩展类有如下几个:
FaultToleranceModule
LookoutModule
RestTracerModule
SofaTracerModule然后会去获取MODULE_LOAD_LIST配置类,多个配置用“;”分割。
调用loader.getAllExtensions()获取所有的扩展类。遍历扩展类。
接着调用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。
- 调用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源码解析系列:
6.源码分析---和dubbo相比SOFARPC是如何实现负载均衡的?
8.源码分析---从设计模式中看SOFARPC中的EventBus?的更多相关文章
- ABP源码分析十五:ABP中的实用扩展方法
类名 扩展的类型 方法名 参数 作用 XmlNodeExtensions XmlNode GetAttributeValueOrNull attributeName Gets an attribu ...
- ABP源码分析四十七:ABP中的异常处理
ABP 中异常处理的思路是很清晰的.一共五种类型的异常类. AbpInitializationException用于封装ABP初始化过程中出现的异常,只要抛出AbpInitializationExce ...
- 6.源码分析---和dubbo相比SOFARPC是如何实现负载均衡的?
官方目前建议使用的负载均衡包括以下几种: random(随机算法) localPref(本地优先算法) roundRobin(轮询算法) consistentHash(一致性hash算法) 所以我们接 ...
- Spring源码分析(十三)缓存中获取单例bean
摘要:本文结合<Spring源码深度解析>来分析Spring 5.0.6版本的源代码.若有描述错误之处,欢迎指正. 介绍过FactoryBean的用法后,我们就可以了解bean加载的过程了 ...
- WorkerMan源码分析(resetStd方法,PHP中STDIN, STDOUT, STDERR的重定向)
WorkerMan中work.php中 resetStd 方法中代码如下 public static function resetStd() { if (!static::$daemonize || ...
- jQuery源码分析--为什么在参数列表中传入undefined
(function(window, undefined){ //jQuery code; })(window); 为什么要传入undefined? 1.没有传入undefined: <!DOCT ...
- Netty源码分析之ByteBuf(一)—ByteBuf中API及类型概述
ByteBuf是Netty中主要的数据容器与操作工具,也是Netty内存管理优化的具体实现,本章我们先从整体上对ByteBuf进行一个概述: AbstractByteBuf是整个ByteBuf的框架类 ...
- 9.源码分析---SOFARPC是如何实现故障剔除的?
SOFARPC源码解析系列: 1. 源码分析---SOFARPC可扩展的机制SPI 2. 源码分析---SOFARPC客户端服务引用 3. 源码分析---SOFARPC客户端服务调用 4. 源码分析- ...
- 11.源码分析---SOFARPC数据透传是实现的?
先把栗子放上,让大家方便测试用: Service端 public static void main(String[] args) { ServerConfig serverConfig = new S ...
随机推荐
- JavaScript 之有趣的函数(函数声明、调用、预解析、作用域)
前言:“函数是对象,函数名是指针.”,函数名仅仅是指向函数的指针,与其他包含函数指针的变量没有什么区别,话句话说,一个函数可能有多个名字. -1.函数声明,function+函数名称.调用方法:函数名 ...
- Go - 循环
目录 概述 循环 array 循环 slice 循环 map break continue goto switch 推荐阅读 概述 前几篇文章分享了 array 数组.slice 切片.map 集合, ...
- 安装Eclipse for MAC 苹果版
1. 安装Eclipse for MAC 苹果版 2. Thank you for downloading Eclipse If the download doesn't start in a few ...
- HTTP、HTTPS常用的默认端口号
端口号标识了一个主机上进行通信的不同的应用程序. 1.HTTP协议代理服务器常用端口号:80/8080/3128/8081/9098 2.SOCKS代理协议服务器常用端口号:1080 3.FTP(文件 ...
- intel FPGA CLKn pin 是否能直接进PLL?
原创 by DeeZeng FPGA的时钟需要从专用的时钟管脚输入,那CLKn 作为Single-End时钟pin时是否能直接进 PLL呢? 通过查看对应FPGA型号的手册,得出以下结论 1. Cyc ...
- 手机如何进入开发者选项--以vivo为例
发现一个新方法 打开拨号键盘 输入 *#*#7777#*#* 欧儿了
- 你懂什么叫js继承吗
说到继承呢?肯定有很多做java的朋友都觉得是一个比较简单的东西了.毕竟面向对象的三大特征就是:封装.继承和多态嘛.但是真正对于一个javascript开发人员来说,很多时候其实你使用了继承,但其实你 ...
- 调用scanf函数的一个陷阱
我们在写C程序时,经常使用scanf函数,让用户输入数据,可是有时候会出现一些很奇怪的问题.例如,下面的程序是一个简单的四则运算: #include <stdio.h> int main( ...
- 搭建oj平台
欢迎使用https://github.com/QingdaoU/OnlineJudgeDeploy
- Ansible配置文件ansible.cfg详解
Ansible是一个系列文章,我会尽量以通俗易懂.诙谐幽默的总结方式给大家呈现这些枯燥的知识点,让学习变的有趣一些. Ansible系列博文直达链接:Ansible入门系列 前言 此时外面小雨淅淅沥沥 ...