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 ...
随机推荐
- 四、Springboot+jpa+mycat应用
一.后台配置文件 # 连接地址 url: jdbc:mysql://127.0.0.1:8066/CHUNK?useUnicode=true&characterEncoding=utf-8&a ...
- remote debug 的详细配置
一.remote debug 的简单介绍 何为远程debug,项目写完后就需要进入到测试环节,将代码打包发布到测试环境(服务器)上.这时候测试人员测试出一个缺陷(bug).由于代码已经发布到测试环境, ...
- 机器学习之近邻算法模型(KNN)
1..导引 如何进行电影分类 众所周知,电影可以按照题材分类,然而题材本身是如何定义的?由谁来判定某部电影属于哪 个题材?也就是说同一题材的电影具有哪些公共特征?这些都是在进行电影分类时必须要考虑的问 ...
- MyBatis Plus 2.3 个人笔记-02-基本注解
实体类注解 /* * MybatisPlus会默认使用实体类的类名到数据中找对应的表. * */ @TableName("tbl_employee") public class E ...
- 遇到过的问题之“解决 No qualifying bean of type 问题”
1.问题 解决 No qualifying bean of type 问题 2.思路: 1 检查是否添加了对应注解 2 检查配置是否正确,扫描包名, 类名及id是否正确 一 . 传统SSM项目 ssm ...
- ubuntu中有关环境变量的问题
ubuntu查看环境变量方法 在Ubuntu中,我们可以使用三个命令来查看当前环境变量的设置,以确定我们有没有把路径加载到环境变量中去.我们 可以使用env,export,或者echo $path, ...
- 浅析CSS定位
position 属性指定了html元素的定位类型. position 属性有 4 种值:1. static(default)表示没有定位,元素出现在正常的文档流中.为静态定位的元素设置 top|bo ...
- 论文阅读-Temporal Phenotyping from Longitudinal Electronic Health Records: A Graph Based Framework
- 人机交互BS
B/S结构用户界面设计 [实验编号] 10003809548j Web界面设计 [实验学时] 8学时 [实验环境] l 所需硬件环境为微机: l 所需软件环境为dreamweaver ...
- iframe引入微信公众号文章
微信在文章页面设置了响应头""frame-ancestors 'self'"阻止了外部页面将其嵌套的行为,文章的图片也设置了防盗链的功能,这就导致了直接在iframe中引 ...