基本介绍在Halo项目中,当用户或博主进行某些操作时,服务器会发布相应的事件,例如博主登录管理员后台,他们会发布“日志记录”事件,当用户浏览一篇文章时会发布一个“访问文章”事件。事件发布后,负责监听的Bean会做相应的处理。这种设计称为事件监听机制。其功能是实现业务逻辑之间的解耦,提高程序的可扩展性和可维护性,ApplicationEvent和ListenerHalo分别使用ApplicationEvent和Listener实现事件的发布和监听,这两者都是Spring提供的,其中ApplicationEvent是需要处理的事件published,Listener为监听器,用户可以在监听器中自定义事件处理逻辑,当事件发生时,只需要发布事件,监听器会自动按照用户自定义的逻辑处理事件。定义一个事件需要继承ApplicationEvent类并重载其构造函数。以LogEvent为例:publicclassLogEventextendsApplicationEvent{privatefinalLogParamlogParam;/***创建一个新的应用程序事件。**@paramsource事件最初发生的对象(永远不会{@codenull})*@paramlogParamloginparam*/publicLogEvent(Objectsource,LogParamlogParam){super(source);//验证日志参数ValidationUtils.validate(logParam);//设置ip地址logParam.setIpAddress(ServletUtils.getRequestIp());this.logParam=logParam;}publicLogEvent(Objectsource,StringlogKey,LogTypelogType,Stringcontent){this(source,newLogParam(logKey,logType,content));}publicLogParamgetLogParam(){返回日志参数;}}构造方法中的source是指触发事件的bean,也称为事件源,通常用this关键字代替,其他参数可由用户指定。发布事件ApplicationContext接口的publishEvent方法可以用来发布事件,比如博客初始化完成后发布LogEvent事件(InstallConroller中的installBlog方法):publicBaseResponseinstallBlog(@RequestBodyInstallParaminstallParam){//省略一些codeeventPublisher.publishEvent(newLogEvent(this,user.getId().toString(),LogType.BLOG_INITIALIZED,"博客已成功初始化"));returnBaseResponse.ok("安装完成!");}Listener创建监听器的方式有多少种,比如实现ApplicationListener接口,SmartApplicationListener接口,或者添加@EventListener注解。项目中使用注解来定义监听器,例如LogEventListener:@ComponentpublicclassLogEventListener{privatefinalLogServicelogService;publicLogEventListener(LogServicelogService){this.logService=logService;}@EventListener@AsyncpublicvoidonApplicationEvent(LogEventevent){//转换为logLoglogToCreate=event.getLogParam().convertTo();//创建日志logService.create(logToCreate);}}用户可以在@EventListener注解修饰的方法中定义事件处理逻辑,该方法接收的参数是要监听的事件类型。@Async注解的作用是实现异步监听。以上面的installBlog方法为例。如果不加这个注解,程序需要等待onApplicationEvent方法执行完成后才返回“安装完成!”。添加@Async注解后,onApplicationEvent方法会在新的线程中执行,installBlog方法可以立即返回。要使@Async注解生效,还需要在启动类或配置类中添加@EnableAsync注解。事件处理下面我们来分析一下Halo项目中不同事件的处理过程:日志事件LogEvent是由LogEventListener中的onApplicationEvent方法处理的。该方法的处理逻辑很简单,就是在logs表中插入一条系统日志,插入的记录用于在管理员界面显示:需要注意的是不同类型日志的logKey、logType和内容会不一样。例如用户登录时,logKey为用户的userName,logType为LogType.LOGGED_IN,内容为用户的昵称:eventPublisher.publishEvent(newLogEvent(this,user.getUsername(),LogType.LOGGED_IN,user.getNickname()));发布文章时,logKey为文章id,logType为LogType.POST_PUBLISHED,content为文章标题:LogEventlogEvent=newLogEvent(this,createdPost.getId().toString(),LogType.POST_PUBLISHED,createdPost.getTitle());eventPublisher.publishEvent(logEvent);文章访问事件PostVisitEvent由AbstractVisitEventListener处理中的handleVisitEvent方法控制,该方法的处理逻辑是将当前文章的访问次数加一:event,"访问事件不能为空");//获取文章id//获取文章idIntegerid=event.getId();升og.debug("收到访问事件,postid:[{}]",id);//如果当前postId有对应的BlockingQueue,则直接返回BlockingQueue,否则为当前postId新建一个BlockingQueue//获取访问后队列BlockingQueuepostVisitQueue=visitQueueMap.computeIfAbsent(id,this::createEmptyQueue);//如果当前postId有对应的PostVisitTask,什么也不做,否则为当前postId新建一个PostVisitTask任务visitTaskMap.computeIfAbsent(id,this::createPostVisitTask);//将当前的postId存入对应的BlockingQueue//对帖子进行访问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创建的线程池ThreadPoolTask??Executor中的线程执行,所以当一篇文章被多个用户同时浏览时,可能会在visitQueueMap中同时创建ThreadPoolTask??Executor中的多个线程时间阻塞队列,或者在visitTaskMap中创建一个PostVisitTask任务。我们看一下PostVisitTask任务中run方法的处理逻辑:);整数postId=postVisitQueue.take();log.debug("重新访问帖子ID:[{}]",postId);//增加访问basePostService.increaseVisit(postId);log.debug("帖子ID的访问量增加:[{}]",postId);}catch(InterruptedExceptione){log.debug("访问后任务:"+Thread.currentThread().getName()+"被中断",e);//忽略此异常}}log.debug("Thread:[{}]hasbeeninterrupted",Thread.currentThread().getName());}线程池ThreadPoolExecutor中的一个线程处理任务:获取阻塞队列对应visitQueueMap中的postId(这里的id其实就是postId),取出队列的第一个元素。将postId对应的文章点赞数加一。只要线程不中断,就会重复步骤1和步骤2。如果队列为空,则线程进入阻塞状态。综上,文章访问事件的处理流程总结如下:当一篇id为postId的文章被访问时,系统会创建一个LinkedBlockingQueue类型的阻塞队列和一个PostVisitTask任务负责增加文章的点赞数一。然后将postId入队,线程池ThreadPoolExecutor分配一个线程执行PostVisitTask任务,阻塞队列中有多少postId就执行多少次任务。结语事件监听机制是一个很重要的知识点。在实际开发中,如果有些业务处理起来比较耗时,而且和主业务的关联性不大,那么可以考虑拆分任务,使用事件监听机制将串行执行异步化,改成并行执行(当然,消息也可以使用队列)。Halo中还有新评论和主题更新等事件。这些事件的处理思路与文章访问事件类似,本文不再过多阐述(⊙?⊙)。