Halo 开源项目学习(六):事件监听机制
基本介绍
Halo 项目中,当用户或博主执行某些操作时,服务器会发布相应的事件,例如博主登录管理员后台时发布 "日志记录" 事件,用户浏览文章时发布 "访问文章" 事件。事件发布后,负责监听的 Bean 会做出相应的处理,这种设计称为事件监听机制,其作用是可以实现业务逻辑之间的解耦,提高程序的扩展性和可维护性。
ApplicationEvent 和 Listener
Halo 使用 ApplicationEvent 和 Listener 来实现事件的发布与监听,二者由 Spring 提供,其中 ApplicationEvent 是需要发布的事件,Listener 则是监听器。用户可在监听器中自定义事件的处理逻辑,当事件发生时,只需要将事件发布,监听器会根据用户定义的逻辑自动处理该事件。
定义事件
事件需要继承 ApplicationEvent 类,且需要重载构造方法,以 LogEvent 为例:
public class LogEvent extends ApplicationEvent {
private final LogParam logParam;
/**
* Create a new ApplicationEvent.
*
* @param source the object on which the event initially occurred (never {@code null})
* @param logParam login param
*/
public LogEvent(Object source, LogParam logParam) {
super(source);
// Validate the log param
ValidationUtils.validate(logParam);
// Set ip address
logParam.setIpAddress(ServletUtils.getRequestIp());
this.logParam = logParam;
}
public LogEvent(Object source, String logKey, LogType logType, String content) {
this(source, new LogParam(logKey, logType, content));
}
public LogParam getLogParam() {
return logParam;
}
}
构造方法中的 source 指的是触发事件的 Bean,也称为事件源,通常用 this 关键字代替,其它参数可由用户任意指定。
发布事件
ApplicationContext 接口的 publishEvent 方法可用于发布事件,例如博客初始化完成后发布 LogEvent 事件(InstallConroller 中的 installBlog 方法):
public BaseResponse<String> installBlog(@RequestBody InstallParam installParam) {
// 省略部分代码
eventPublisher.publishEvent(
new LogEvent(this, user.getId().toString(), LogType.BLOG_INITIALIZED, "博客已成功初始化")
);
return BaseResponse.ok("安装完成!");
}
监听器
监听器的创建方式有多种,例如实现 ApplicationListener 接口、SmartApplicationListener 接口,或者添加 @EventListener 注解。项目中使用注解来定义监听器,如 LogEventListener:
@Component
public class LogEventListener {
private final LogService logService;
public LogEventListener(LogService logService) {
this.logService = logService;
}
@EventListener
@Async
public void onApplicationEvent(LogEvent event) {
// Convert to log
Log logToCreate = event.getLogParam().convertTo();
// Create log
logService.create(logToCreate);
}
}
用户可在 @EventListener 注解修饰的方法中定义事件的处理逻辑,方法接收的参数为监听的事件类型。@Async 注解的作用是实现异步监听,以上文中的 installBlog 方法为例,如果不添加该注解,那么程序需要等待 onApplicationEvent 方法执行结束后才能返回 "安装完成!"。加上 @Async 注解后,onApplicationEvent 方法会在新的线程中执行,installBlog 方法可以立即返回。若要使 @Async 注解生效,还需要在启动类或配置类上添加 @EnableAsync 注解。
事件处理
接下来我们分析一下 Halo 项目中不同事件的处理过程:
日志记录事件
日志记录事件 LogEvent 由 LogEventListener 中的 onApplicationEvent 方法处理,该方法的处理逻辑非常简单,就是在 logs 表中插入一条系统日志,插入的记录用于在管理员界面展示:

需要注意的是,不同类型日志的 logKey、logType 以及 content 会有所区别,例如用户登录时,logKey 为用户的 userName,logType 为 LogType.LOGGED_IN,content 为用户的 nickName:
eventPublisher.publishEvent(
new LogEvent(this, user.getUsername(), LogType.LOGGED_IN, user.getNickname()));
发布文章时,logKey 为文章的 id,logType 为 LogType.POST_PUBLISHED,content 为文章的 title:
LogEvent logEvent = new LogEvent(this, createdPost.getId().toString(),
LogType.POST_PUBLISHED, createdPost.getTitle());
eventPublisher.publishEvent(logEvent);
文章访问事件
文章访问事件 PostVisitEvent 由 AbstractVisitEventListener 中的 handleVisitEvent 方法处理,该方法的处理的逻辑是将当前文章的访问量加一:
protected void handleVisitEvent(@NonNull AbstractVisitEvent event) throws InterruptedException {
Assert.notNull(event, "Visit event must not be null");
// 获取文章 id
// Get post id
Integer id = event.getId();
log.debug("Received a visit event, post id: [{}]", id);
// 如果当前 postId 具有对应的 BlockingQueue, 那么直接返回该 BlockingQueue, 否则为当前 postId 创建一个新的 BlockingQueue
// Get post visit queue
BlockingQueue<Integer> postVisitQueue =
visitQueueMap.computeIfAbsent(id, this::createEmptyQueue);
// 如果当前 postId 具有对应的 PostVisitTask, 不做任何处理, 否则为当前 postId 创建一个新的 PostVisitTask 任务
visitTaskMap.computeIfAbsent(id, this::createPostVisitTask);
// 将当前 postId 存入到对应的 BlockingQueue
// Put a visit for the post
postVisitQueue.put(id);
}
上述方法首先获取当前被访问文章的 postId,然后查询 visitQueueMap 中是否存在 postId 对应的阻塞队列(实际类型为 LinkedBlockingQueue),如果存在那么直接返回该队列, 否则为当前 postId 创建一个新的阻塞队列并存入到 visitQueueMap。接着查询 visitTaskMap 中是否存在 postId 对应的 PostVisitTask 任务(任务的作用是将文章的访问量加一),如果没有,那么就为 postId 创建一个新的 PostVisitTask 任务,并将该任务交给线程池 ThreadPoolExecutor(Executors.newCachedThreadPool())执行。之后将 postId 添加到对应的阻塞队列,这一步的目的是管理 PostVisitTask 任务的执行次数。
visitQueueMap 和 visitTaskMap 都是 ConcurrentHashMap 类型的对象,使用 ConcurrentHashMap 是为了保证线程安全,因为监听器的事件处理方法被 @Async 注解修饰。默认情况下,@Async 注解修饰的方法会由 Spring 创建的线程池 ThreadPoolTaskExecutor 中的线程执行,因此当某一篇文章被多个用户同时浏览时,ThreadPoolTaskExecutor 中的多个线程可能会同时在 visitQueueMap 中创建阻塞队列,或在 visitTaskMap 中创建 PostVisitTask 任务。
下面看一下 PostVisitTask 任务中 run 方法的处理逻辑:
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
BlockingQueue<Integer> postVisitQueue = visitQueueMap.get(id);
Integer postId = postVisitQueue.take();
log.debug("Took a new visit for post id: [{}]", postId);
// Increase the visit
basePostService.increaseVisit(postId);
log.debug("Increased visits for post id: [{}]", postId);
} catch (InterruptedException e) {
log.debug(
"Post visit task: " + Thread.currentThread().getName() + " was interrupted",
e);
// Ignore this exception
}
}
log.debug("Thread: [{}] has been interrupted", Thread.currentThread().getName());
}
线程池 ThreadPoolExecutor 中的一个线程处理该任务:
从 visitQueueMap 获取 postId 对应的阻塞队列(这里的 id 其实就是 postId),并取出队首元素。
将 postId 对应的文章的点赞量加一。
只要线程不被中断,就一直重复步骤 1 和步骤 2,如果队列为空,那么线程进入阻塞。
综上,文章访问事件的处理流程总结如下:
当 id 为 postId 的文章被访问时,系统会为其创建一个 LinkedBlockingQueue 类型的阻塞队列和一个负责将文章点赞量加一的 PostVisitTask 任务。然后 postId 入队,线程池 ThreadPoolExecutor 分配一个线程执行 PostVisitTask 任务,阻塞队列有多少个 postId 该任务就执行多少次。

结语
事件监听机制是一个非常重要的知识点,实际开发中,如果某些业务处理起来比较耗时,且与主要业务的关联性并不是很强,那么可以考虑做任务拆分,利用事件监听机制将串行执行异步化,改为并行执行(当然也可以使用消息队列)。Halo 中还有新增评论、主题更新等事件,这些事件的的处理思路与文章访问事件相似,所以本文就不再过多陈述了 ( ⊙‿⊙)。
Halo 开源项目学习(六):事件监听机制的更多相关文章
- Halo 开源项目学习(七):缓存机制
基本介绍 我们知道,频繁操作数据库会降低服务器的系统性能,因此通常需要将频繁访问.更新的数据存入到缓存.Halo 项目也引入了缓存机制,且设置了多种实现方式,如自定义缓存.Redis.LevelDB ...
- JAVA事件监听机制学习
//事件监听机制 import java.awt.*; import java.awt.event.*; public class TestEvent { public static void mai ...
- java Gui编程 事件监听机制
1. GUI编程引言 以前的学习当中,我们都使用的是命令交互方式: 例如:在DOS命令行中通过javac java命令启动程序. 软件的交互的方式: 1. 命令交互方式 图书管理系统 ...
- JAVA之旅(三十一)——JAVA的图形化界面,GUI布局,Frame,GUI事件监听机制,Action事件,鼠标事件
JAVA之旅(三十一)--JAVA的图形化界面,GUI布局,Frame,GUI事件监听机制,Action事件,鼠标事件 有段时间没有更新JAVA了,我们今天来说一下JAVA中的图形化界面,也就是GUI ...
- Java 中的事件监听机制
看项目代码时遇到了好多事件监听机制相关的代码.现学习一下: java事件机制包含三个部分:事件.事件监听器.事件源. 1.事件:继承自java.util.EventObject类,开发人员自己定义. ...
- JAVA事件监听机制的实现
今天学习了java的事件编程机制,略有体会,先在此记下心得. 第一,首先明确几个概念. 事件源:一个产生或者触发事件的对象.事件:承载事件源状态改变时的信息对象.事件监听器接口:实际上就是一个类,该类 ...
- SpringBoot事件监听机制源码分析(上) SpringBoot源码(九)
SpringBoot中文注释项目Github地址: https://github.com/yuanmabiji/spring-boot-2.1.0.RELEASE 本篇接 SpringApplicat ...
- 4.JAVA之GUI编程事件监听机制
事件监听机制的特点: 1.事件源 2.事件 3.监听器 4.事件处理 事件源:就是awt包或者swing包中的那些图形用户界面组件.(如:按钮) 事件:每一个事件源都有自己特点有的对应事件和共性事件. ...
- .NET事件监听机制的局限与扩展
.NET中把“事件”看作一个基本的编程概念,并提供了非常优美的语法支持,对比如下C#和Java代码可以看出两种语言设计思想之间的差异. // C#someButton.Click += OnSomeB ...
随机推荐
- Spring 和 SpringBoot 有什么不同?
Spring 框架提供多种特性使得 web 应用开发变得更简便,包括依赖注入.数据绑定.切面编程.数据存取等等. 随着时间推移,Spring 生态变得越来越复杂了,并且应用程序所必须的配置文件也令人觉 ...
- 面试问题之C++语言:类模板声明与定义为何不能分开
C++中每个对象所占用的空间大小,是在编译的时候就确定的,在模板类没有真正的被使用之前,编译器是无法知道,模板类中使用模板类型的对象的所占用的空间的大小的.只有模板被真正使用的时候,编译器才知道,模板 ...
- 说一下 jvm 有哪些垃圾回收器?
新生代收集器: SerialParNewParallel Scavenge 老年代收集器: Serial OldCMSParallel Old 堆内存垃圾收集器: G1 参考链接:JVM常见的垃圾回收 ...
- 解释 Spring 框架中 bean 的生命周期?
Spring 容器 从 XML 文件中读取 bean 的定义,并实例化 bean. Spring 根据 bean 的定义填充所有的属性. 如果 bean 实现了 BeanNameAware 接口,Sp ...
- hanoi(汉诺塔)递归实现
汉诺塔:汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具.大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘.大梵天命令婆罗门把圆盘从下面开始按大小顺序 ...
- Altium Designer 原理图的绘制前导
元件库.封装库设计 部分元器件厂商或者经销商不提供元件库和封装库,只给了元器件尺寸图,所以需要自行设计元件库文件或是封装库文件 元件库设计: 新建 .SchLib 文件:File -> N ...
- ionic3 教程(五)基本的网络请求
链接: ionic3教程(一)安装和配置 ionic3教程(二)登录页制作 ionic3教程(三)设置页制作 ionic3教程(四)安卓硬件返回键处理ionic3 教程(五)基本的网络请求 这是最后一 ...
- Pullword 中文分词
安装 npm install pullword 使用 var defaultOptions = { url: 'http://api.pullword.com/post.php', /* api ...
- 【Android开发】控件外边框自定义
1.在drawable里面新建自定义的资源文件shape <?xml version="1.0" encoding="utf-8"?> <sh ...
- Python pip下载慢的解决方法
国外的源下载速度实在是太慢了 可以使用国内的一些镜像网站安装 使用cmd命令 格式:pip install -i 网站 库 例如: 国内的一些镜像网站 清华大学:https://pypi.tuna.t ...