Event事件机制

Spring事件机制基于观察者模式实现了事件发布和监听的功能,Spring有一些框架内部的事件,我们也可以自定义事件。使用事件机制的好处是能够进一步解耦业务逻辑,实现低耦合、无侵入式的业务逻辑处理,也可以实现异步处理等高级功能。

基本概念

事件:Spring中的事件ApplicationEvent继承JDK的java.util.EventObject抽象类,该类包含了一个source属性保存了事件源对象的引用,以及事件发生的时间戳,Spring内置的事件和我们自定义的事件需要继承ApplicationEvent这个类来实现。

事件发布者:用于发布事件的接口,Spring框架中我们可以调用applicationContext.publishEvent()发布事件。

事件监听器:事件监听器可以使用@EventListener注解或实现ApplicationListener接口来定义,注意它必须是一个Spring的Bean,否则Spring框架是无法找到指定的监听器对象的。

事件机制的使用

监听Spring内置事件

Spring内置的事件和发布者都是Spring实现的,因此我们编写一个事件监听器即可。

package com.gacfox.demo;

import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class DemoComponent {
    @EventListener
    public void onStarted(ApplicationStartedEvent event) {
        System.out.println("Spring启动完成");
    }
}

代码中我们使用了@EventListener注解标注了一个方法,Spring会根据方法的参数类型,在对应事件发生时回调方法。如果参数类型使用ApplicationEvent,则所有类型的事件该方法都会接收到。

此外要注意,如果有多个方法监听同一个事件,事件发生时所有方法都会被回调,不会出现只有一个方法被调用的情况。

Spring中常用的内置事件有如下几种。

事件类型 说明
ContextRefreshedEvent 当Spring容器初始化或刷新时触发,即applicationContext的refresh()方法被调用后。
ContextStartedEvent 当Spring容器停止时触发,即applicationContext的start()方法被调用后。
ContextStoppedEvent 当Spring容器关闭时触发,即applicationContext的stop()方法被调用后。
ContextClosedEvent 当Spring容器启动时触发,即applicationContext的close()方法被调用后。
RequestHandledEvent 用于SpringMVC环境,当DispatcherServlet处理请求完成后触发。

自定义事件

自定义事件和使用内置事件类似,只不过需要我们手动创建EventObject和发射事件,下面是一个自定义事件的例子。

RegistrationEvent.java

package com.gacfox.demo;

import org.springframework.context.ApplicationEvent;

public class RegistrationEvent extends ApplicationEvent {
    public RegistrationEvent(Object source) {
        super(source);
    }
}

DemoComponent.java

package com.gacfox.demo;

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class DemoComponent {
    @EventListener
    public void onRegistration(RegistrationEvent event) {
        System.out.println("用户注册了");
    }
}

Main.java

package com.gacfox.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

@Slf4j
public class Main {
    private static ApplicationContext context;

    public static void main(String[] args) {
        context = new ClassPathXmlApplicationContext("applicationContext.xml");
        new Main().testPublishEvent();
    }

    private void testPublishEvent() {
        context.publishEvent(new RegistrationEvent(this));
    }
}

代码中,RegistrationEvent是我们自定义的事件,它继承了ApplicationEvent,DemoComponent是我们的事件监听器,最后Main类被我们作为了发射器,它通过applicationContext对象发射事件,并由DemoComponent的onRegistration()方法接收。

异步事件

上面介绍的自定义事件监听例子其实还是一个同步式的代码,如果我们的EventListener处理事件的耗时较长,事件的发射端必须等待它处理完成后才能返回。实际上,Spring的事件机制也可以结合异步任务实现事件的异步处理,这需要我们配置异步任务声明注解的扫描和异步执行器。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:task="http://www.springframework.org/schema/task"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd">
    <context:component-scan base-package="com.gacfox.demo"/>
    <task:annotation-driven executor="appTaskExecutor"/>
    <bean id="appTaskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
        <property name="corePoolSize" value="5"/>
        <property name="maxPoolSize" value="10"/>
        <property name="queueCapacity" value="100"/>
        <property name="threadNamePrefix" value="AsyncTask-"/>
        <property name="rejectedExecutionHandler">
            <bean class="java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy"/>
        </property>
    </bean>
</beans>

此时事件的接收端增加一个@Async注解即可,不需要其它改动。

package com.gacfox.demo;

import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
public class DemoComponent {
    @Async
    @EventListener
    public void onRegistration(RegistrationEvent event) {
        try {
            // 模拟耗时操作
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("用户注册了");
    }
}

我们可以通过打印线程ID和线程名来验证是否实现了异步执行,如果异步执行,事件发布的线程和处理事件的线程应该并非同一个。

注意:在Web工程中,由于Java的Servlet API从设计之初就采用的是多线程并发模型,我们对请求进行的处理流程都是阻塞式、串行的,所有处理都发生在同一个线程中,因此许多库和框架都是依赖ThreadLocal构建的,比如SpringMVC的RequestContextHolder、日志框架的MDC等。如果引入异步处理这些API就会失效,我们需要考虑到这些潜在的问题。

有关异步任务调度的详细内容可以参考后续任务调度章节。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。