基本介绍

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 中的一个线程处理该任务:

  1. 从 visitQueueMap 获取 postId 对应的阻塞队列(这里的 id 其实就是 postId),并取出队首元素。

  2. 将 postId 对应的文章的点赞量加一。

  3. 只要线程不被中断,就一直重复步骤 1 和步骤 2,如果队列为空,那么线程进入阻塞。

综上,文章访问事件的处理流程总结如下:

当 id 为 postId 的文章被访问时,系统会为其创建一个 LinkedBlockingQueue 类型的阻塞队列和一个负责将文章点赞量加一的 PostVisitTask 任务。然后 postId 入队,线程池 ThreadPoolExecutor 分配一个线程执行 PostVisitTask 任务,阻塞队列有多少个 postId 该任务就执行多少次。

结语

事件监听机制是一个非常重要的知识点,实际开发中,如果某些业务处理起来比较耗时,且与主要业务的关联性并不是很强,那么可以考虑做任务拆分,利用事件监听机制将串行执行异步化,改为并行执行(当然也可以使用消息队列)。Halo 中还有新增评论、主题更新等事件,这些事件的的处理思路与文章访问事件相似,所以本文就不再过多陈述了 ( ⊙‿⊙)。

Halo 开源项目学习(六):事件监听机制的更多相关文章

  1. Halo 开源项目学习(七):缓存机制

    基本介绍 我们知道,频繁操作数据库会降低服务器的系统性能,因此通常需要将频繁访问.更新的数据存入到缓存.Halo 项目也引入了缓存机制,且设置了多种实现方式,如自定义缓存.Redis.LevelDB ...

  2. JAVA事件监听机制学习

    //事件监听机制 import java.awt.*; import java.awt.event.*; public class TestEvent { public static void mai ...

  3. java Gui编程 事件监听机制

    1.     GUI编程引言 以前的学习当中,我们都使用的是命令交互方式: 例如:在DOS命令行中通过javac java命令启动程序. 软件的交互的方式:   1. 命令交互方式    图书管理系统 ...

  4. JAVA之旅(三十一)——JAVA的图形化界面,GUI布局,Frame,GUI事件监听机制,Action事件,鼠标事件

    JAVA之旅(三十一)--JAVA的图形化界面,GUI布局,Frame,GUI事件监听机制,Action事件,鼠标事件 有段时间没有更新JAVA了,我们今天来说一下JAVA中的图形化界面,也就是GUI ...

  5. Java 中的事件监听机制

    看项目代码时遇到了好多事件监听机制相关的代码.现学习一下: java事件机制包含三个部分:事件.事件监听器.事件源. 1.事件:继承自java.util.EventObject类,开发人员自己定义. ...

  6. JAVA事件监听机制的实现

    今天学习了java的事件编程机制,略有体会,先在此记下心得. 第一,首先明确几个概念. 事件源:一个产生或者触发事件的对象.事件:承载事件源状态改变时的信息对象.事件监听器接口:实际上就是一个类,该类 ...

  7. SpringBoot事件监听机制源码分析(上) SpringBoot源码(九)

    SpringBoot中文注释项目Github地址: https://github.com/yuanmabiji/spring-boot-2.1.0.RELEASE 本篇接 SpringApplicat ...

  8. 4.JAVA之GUI编程事件监听机制

    事件监听机制的特点: 1.事件源 2.事件 3.监听器 4.事件处理 事件源:就是awt包或者swing包中的那些图形用户界面组件.(如:按钮) 事件:每一个事件源都有自己特点有的对应事件和共性事件. ...

  9. .NET事件监听机制的局限与扩展

    .NET中把“事件”看作一个基本的编程概念,并提供了非常优美的语法支持,对比如下C#和Java代码可以看出两种语言设计思想之间的差异. // C#someButton.Click += OnSomeB ...

随机推荐

  1. idea执行maven命令的三种方式

    前言: java开发的IDE工具idea默认会提供maven生命周期的图形化执行,但是如果我们需要定制化的执行命令的时候,就需要使用手动执行maven命令的方式,今天就和大家讲一下idea手动执行ma ...

  2. 重写hashcode和equals方法

    重写hashcode和equals方法 简乐君 2019-05-07 21:55:43 35481 收藏 191分类专栏: Java 文章标签: equals() hashcode()版权 一.前言我 ...

  3. 在JAVA中如何跳出当前的多重嵌套循环?

    在Java中,要想跳出多重循环,可以在外面的循环语句前定义一个标号,然后在里层循环体的代码中使用带有标号的break语句,即可跳出外层循环.

  4. Oracle入门基础(二)一一过滤和排序

    SQL> --查询10号部门的员工 SQL> select * from emp where deptno=10; EMPNO ENAME JOB MGR HIREDATE SAL COM ...

  5. CyclicBarrier 和 CountDownLatch 的区别 ?

    1.CountDownLatch 简单的说就是一个线程等待,直到他所等待的其他线程都执 行完成并且调用 countDown()方法发出通知后,当前线程才可以继续执行. 2.cyclicBarrier ...

  6. Replicated State Machine和WAL

    在阅读raft论文的时候,考虑两个问题: 为什么要用Replicated State Machine?没有其他方式吗 为什么要先写日志再应用到Replicated State Machine,直接应用 ...

  7. 创建Maven web工程

    ---恢复内容开始--- 第一步,启动Eclipse,依次打开菜单[File][New][Other] 找到目录Maven,选择Maven Project, 选择一个Archetype.这里创建Web ...

  8. JVM原理与深度调优

    什么是jvm jvm是java虚拟机 运行在用户态.通过应用程序实现java代码跨平台.与平台无关.实际上是"一次编译,到处执行" 1.从微观来说编译出来的是字节码!去到哪个平台都 ...

  9. (HTML基础系列1)网页基本结构

    元素名 描述 header 标记头部区域内容 footer 标记脚部区域内容 section Web页面中独立的一块区域 article 独立的文章内容 aside 相关文章应用或应用(常用于侧边栏) ...

  10. python学习笔记(十)——进程间通信

    python 在进程间通信时有很多方式,比如使用Queue的消息队列,使用 pip的管道通信,share memory 共享内存或 semaphore 信号量等通信方式. 这里我们演示一下通过消息队列 ...