Spring系列17:Spring事件发布详解

1,211 阅读8分钟

本文内容

  1. 实现原理和标准事件
  2. 编程式实现自定义事件
  3. 基于注解的自定义事件
  4. 通用事件

实现原理和标准事件

Spring中的事件发布本质上是标准的观察者设计模式。ApplicationContext 中的事件处理是通过 ApplicationEvent 类和 ApplicationListener 接口提供的。如果将实现 ApplicationListener 接口的bean部署到上下文中,则每次将 ApplicationEvent 发布到 ApplicationContext 时,该bean都会得到通知。

public abstract class ApplicationEvent extends EventObject {

    // 事件源
	public ApplicationEvent(Object source) {
		super(source);
		this.timestamp = System.currentTimeMillis();
	}

	public final long getTimestamp() {
		return this.timestamp;
	}

}
@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {

	// 处理事件
	void onApplicationEvent(E event);

}

下表描述了 Spring 提供的标准事件:

事件解释
ContextRefreshedEvent在初始化或刷新时发布ApplicationContext(例如,通过使用接口refresh()上的方法ConfigurableApplicationContext)。这里,“初始化”意味着所有 bean 都已加载,后处理器 bean 被检测并激活,单例被预先实例化,并且ApplicationContext对象已准备好使用。只要上下文没有关闭,就可以多次触发刷新,前提是所选择的ApplicationContext实际支持这种“热”刷新。例如,XmlWebApplicationContext支持热刷新,但 GenericApplicationContext不支持。
ContextStartedEvent使用接口上的方法 ApplicationContext启动时发布。在这里,“已启动”意味着所有 bean 都接收到一个明确的启动信号。通常,此信号用于在显式停止后重新启动 bean,但它也可用于启动尚未配置为自动启动的组件(例如,尚未在初始化时启动的组件)。start()``ConfigurableApplicationContext``Lifecycle
ContextStoppedEvent使用接口上的方法 ApplicationContext停止时发布。在这里,“停止”意味着所有 的 bean 都会收到一个明确的停止信号。可以通过 调用重新启动已停止的上下文。stop()``ConfigurableApplicationContext``Lifecycle``start()
ContextClosedEventApplicationContext使用接口close()上的方法ConfigurableApplicationContext或通过 JVM 关闭挂钩关闭时发布。在这里,“关闭”意味着所有的单例 bean 都将被销毁。一旦上下文关闭,它就到了生命的尽头,无法刷新或重新启动。
RequestHandledEvent一个特定于 Web 的事件,告诉所有 bean 一个 HTTP 请求已得到服务。此事件在请求完成后发布。此事件仅适用于使用 Spring 的 Web 应用程序DispatcherServlet
ServletRequestHandledEvent它的子类RequestHandledEvent添加了 Servlet 特定的上下文信息。

编程式实现自定义事件

  1. 定义事件继承 ApplicationEvent

    public class BlockedListEvent extends ApplicationEvent {
    
        private final String address;
        private final String content;
    
        public BlockedListEvent(Object source, String address, String content) {
            super(source);
            this.address = address;
            this.content = content;
        }
    
        public String getAddress() {
            return address;
        }
    
        public String getContent() {
            return content;
        }
    }
    
  2. 定义事件监听器实现 ApplicationListener

    @Component
    public class BlockedListListener implements ApplicationListener<BlockedListEvent> {
        // @Async
        @Override
        public void onApplicationEvent(BlockedListEvent event) {
            System.out.println("收到邮件禁用发送通知,地址: " + event.getAddress() + "  内容: " + event.getContent());
    
        }
    }
    
  3. 业务类注入 ApplicationEventPublisher 用于发布事件

    @Component
    public class EmailSendService {
        @Autowired
        private ApplicationEventPublisher publisher;
        // 邮件黑名单
        private List<String> blockedList;
    
    
        public void sendEmail(String address, String content) {
            if (blockedList.contains(address)) {
                System.out.println("不允许发邮件,同步发布通知事件");
                publisher.publishEvent(new BlockedListEvent(this, address, content));
                System.out.println("同步发布通知事件结束");
                return;
            }
            // 允许
            System.out.println("允许发邮件");
        }
        public void setBlockedList(List<String> blockedList) {
            this.blockedList = blockedList;
        }
    }
    
  4. 测试程序和结果

    @Test
    public void test_synchronous() {
        AnnotationConfigApplicationContext context =
                new AnnotationConfigApplicationContext(AppConfig.class);
        EmailSendService sendService = context.getBean(EmailSendService.class);
        sendService.setBlockedList(Arrays.asList("123"));
        sendService.sendEmail("123","ooo");
        sendService.sendEmail("456","ooo");
    }
    
    不允许发邮件,同步发布通知事件
    收到邮件禁用发送通知,地址: 123  内容: ooo
    同步发布通知事件结束
    允许发邮件
    

    从结果分析,publishEvent()发布事件的方式是阻塞同步的,期间会等待所有的事件监听器完成处理。

基于注解的自定义事件

从Spring 4.2开始,事件基础结构得到了显著的改进,并提供了一个基于注释的模型,以及发布任何任意事件(也就是说,一个不一定是从ApplicationEvent扩展而来的对象)的能力。当这样一个对象被发布时会被包装在一个事件中。

可以使用 @EventListener 注释在托管 bean 的任何方法上注册事件监听器。

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EventListener {

   /**
    * Alias for {@link #classes}.
    */
   @AliasFor("classes")
   Class<?>[] value() default {};

   // 监听哪些事件
   // 如果使用单个值指定该属性,则带注释的方法可以选择接受单个参数。
   // 但是,如果使用多个值指定此属性,则带注释的方法不能声明任何参数
   @AliasFor("value")
   Class<?>[] classes() default {};

   // SpEL表达式条件
   String condition() default "";

}

监听单个事件

@Component
public class AnnotatedBlockedListListener{

    @EventListener(value = BlockedListEvent.class)
    public void onApplicationEvent(BlockedListEvent event) {
        System.out.println("AnnotatedBlockedListListener收到邮件禁用发送通知,地址: " + event.getAddress() + "  内容: " + event.getContent());
    }
}

监听多个事件

如果使用多个值指定监听多个事件,则带注释的方法不能声明任何参数。

@Component
public class MultiEventListener {
    @EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
    public void handleContextStart() {
        System.out.println("收到 ContextStartedEvent 或 ContextRefreshedEvent");
    }
}

监听方法结果作为新事件发布

监听方法处理后返回值如果是 ApplicationEvent 的实现类,Spring会将结果当做新事件来发布。支持单个或是集合形式,集合形式的返回值会拆分成单个依次发送。直接看案例。

  1. 定义一个结果事件 ResultEvent 继承 ApplicationEvent

    public class ResultEvent extends ApplicationEvent {
    
        private final String result;
    
        public ResultEvent(Object source,  String result) {
            super(source);
            this.result = result;
        }
    
    
        public String getResult() {
            return result;
        }
    }
    
  2. 定义 BlockedListEvent 的监听器返回值是单个 ApplicationEvent

    @Component
    public class AnnotatedBlockedListListener2 {
    
        @EventListener(value = BlockedListEvent.class)
        public ResultEvent onApplicationEvent(BlockedListEvent event) {
            System.out.println("邮件处理完毕,返回单个通知ResultEvent事件");
            return new ResultEvent(event.getSource(), "success");
        }
    }
    
  3. 定义 BlockedListEvent 的监听器返回值集合形式的 ApplicationEvent

    @Component
    public class AnnotatedBlockedListListener3 {
    
        @EventListener(value = BlockedListEvent.class)
        public List<ResultEvent> onApplicationEvent(BlockedListEvent event) {
            System.out.println("邮件处理完毕,返回多个通知ResultEvent事件");
            List<ResultEvent > resultEvents = new ArrayList<>(5);
            for (int i = 0; i < 5; i++) {
                resultEvents.add(new ResultEvent(event.getSource(), "success:" + i));
            }
            return resultEvents;
        }
    }
    
    
  4. 定义 ResultEvent 的监听器

    @Component
    public class ResultEventListener {
    
        @EventListener(ResultEvent.class)
        public void onApplicationEvent(ResultEvent event) {
            System.out.println("收到ResultEvent,结果: " + event.getResult());
        }
    }
    
  5. 运行结果

    AnnotatedBlockedListListener收到邮件禁用发送通知,地址: 123  内容: ooo
    邮件处理完毕,返回单个通知ResultEvent事件
    收到ResultEvent,结果: success
    邮件处理完毕,返回多个通知ResultEvent事件
    收到ResultEvent,结果: success:0
    收到ResultEvent,结果: success:1
    收到ResultEvent,结果: success:2
    收到ResultEvent,结果: success:3
    收到ResultEvent,结果: success:4
    收到邮件禁用发送通知,地址: 123  内容: ooo
    同步发布通知事件结束
    允许发邮件
    

    从结果看,AnnotatedBlockedListListener2 和 AnnotatedBlockedListListener3 的监听方法结果被当做新的事件发布了,并被 ResultEventListener 监听到后处理。

使用SpEL表达式

@EventListener 中的 condition 可以指定SpEL表达式条件。如当事件内容是“ooo”时才监听器方法才处理该事件。实现如下

@Component
public class ConditionalBlockedListListener{
    // 当事件内容是ooo 才处理
    @EventListener(value = BlockedListEvent.class, condition = "#event.content == 'ooo'")
    public void onApplicationEvent(BlockedListEvent event) {
        System.out.println("当事件内容是ooo 才处理");
    }
}

下表列出了可用于上下文的项目,可以用于condition 中:

名称位置描述例子
事件root objectApplicationEvent对象#root.event or event
参数数组root object方法的参数数组#root.args 或者args; args[0] 按照索引取
参数名称context任何方法参数的名称。如果,由于某些原因,名称不可用(例如,因为编译后的字节码中没有调试信息),则使用#a<#arg>语法也可以使用单个参数,其中<#arg>表示参数索引(从0开始)。#blEvent 或是#a0

异步事件

默认情况下,上面的案例的事件发布和事件处理都是同步阻塞完成的。可以使用 @Async 来完成异步处理事件。

@Component
@EnableAsync // 启用Spring异步处理
public class AnnotatedBlockedListListener4 {

    @Async // 标记方法为异步
    @EventListener(value = BlockedListEvent.class)
    public void onApplicationEvent(BlockedListEvent event) {
        System.out.println("异步处理收到邮件禁用发送通知,地址: " + event.getAddress() + "  内容: " + event.getContent());
    }
}

使用异步事件时请注意以下限制:

  • 如果异步事件侦听器抛出异常,它不会传播给调用者。需要使用 AsyncUncaughtExceptionHandler 来处理异常,后面专门详解。
  • 异步事件侦听器方法不能通过返回值来发布后续事件。如果需要发布另一个事件作为处理的结果,请注入一个 ApplicationEventPublisher 以手动发布该事件。

使用@Order定义同一事件监听器

同一个事件多个监听器时候,@Order 值较小的先收到。

    @Order(3)
    @EventListener(value = BlockedListEvent.class)
    public void onApplicationEvent(BlockedListEvent event) {
        System.out.println("AnnotatedBlockedListListener收到邮件禁用发送通知,地址: " + event.getAddress() + "  内容: " + event.getContent());
    }

   @Order(2)
    @EventListener(value = BlockedListEvent.class)
    public ResultEvent onApplicationEvent(BlockedListEvent event) {
        System.out.println("邮件处理完毕,返回单个通知ResultEvent事件");
        return new ResultEvent(event.getSource(), "success");
    }

    @Order(1)
    @EventListener(value = BlockedListEvent.class)
    public List<ResultEvent> onApplicationEvent(BlockedListEvent event) {
        System.out.println("邮件处理完毕,返回多个通知ResultEvent事件");
        List<ResultEvent > resultEvents = new ArrayList<>(5);
        for (int i = 0; i < 5; i++) {
            resultEvents.add(new ResultEvent(event.getSource(), "success:" + i));
        }
        return resultEvents;
    }

观察下输出

邮件处理完毕,返回多个通知ResultEvent事件
邮件处理完毕,返回单个通知ResultEvent事件
AnnotatedBlockedListListener收到邮件禁用发送通知,地址: 123  内容: ooo

泛型化事件结构

使用泛型事件

可以使用泛型进一步定义事件的结构。直接看案例。

实体创建事件使用 EntityCreatedEvent<T>,其中 T 是创建的实际实体的类型

public class EntityCreatedEvent<T> extends ApplicationEvent {

    public EntityCreatedEvent(T source) {
        super(source);
    }
}

假设有2个是实体 Order 和 Person, 则它们的创建事件监听方法如下:

    @EventListener(PersonEntityCreatedEvent.class)
    public void onPersonCreated(EntityCreatedEvent<Person> event) {
        System.out.println("onPersonCreated");
    }

    @EventListener(OrderEntityCreatedEvent.class)
    public void onOrderCreated(EntityCreatedEvent<Order> event) {
        System.out.println("onOrderCreated");
    }

由于Java中的类型擦除,这仅在触发的事件解析事件侦听器过滤的通用参数时才有效。对应的定义一下2个类:

public class OrderEntityCreatedEvent extends  EntityCreatedEvent<Order>{
    public OrderEntityCreatedEvent(Order source) {
        super(source);
    }
}

public class PersonEntityCreatedEvent extends EntityCreatedEvent<Person>{
    public PersonEntityCreatedEvent(Person source) {
        super(source);
    }
}

测试程序和测试结果

public class EntityCreatedEventTest {

    @Test
    public void test() {
        AnnotationConfigApplicationContext context =
                new AnnotationConfigApplicationContext(AppConfig.class);
        context.publishEvent(new OrderEntityCreatedEvent(new Order()));
        context.publishEvent(new PersonEntityCreatedEvent(new Person()));
        context.close();
    }
}
onOrderCreated
onPersonCreated

使用ResolvableTypeProvider优化

前面的案例,每个实体类A都需要定义一个对应的具体事件类EntityCreatedEvent<A>,这样模板化的机械化的工作某种程度上非常乏味。在这种情况下,可以实现 ResolvableTypeProvider 来引导框架超出运行时环境提供的范围,就是运行时指定类型。

泛型事件实现 ResolvableTypeProvider

/**
 * 泛型的实体创建事件
 * @author zfd
 * @version v1.0
 * @date 2022/1/26 15:09
 * @关于我 请关注公众号 螃蟹的Java笔记 获取更多技术系列
 */
public class ResolvableEntityCreatedEvent<T> extends ApplicationEvent implements ResolvableTypeProvider {

    public ResolvableEntityCreatedEvent(T source) {
        super(source);
    }

    @Override
    public ResolvableType getResolvableType() {
        return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(getSource()));
    }
}

省去了实体对应具体事件的定义,直接上监听方法

@Component
public class ResolvableEntityCreatedEventListener {

    @EventListener
    public void onPersonCreated(ResolvableEntityCreatedEvent<Person> event) {
        System.out.println("onPersonCreated Resolvable");
    }

    @EventListener
    public void onOrderCreated(ResolvableEntityCreatedEvent<Order> event) {
        System.out.println("onOrderCreated Resolvable");
    }
}

观察下结果

@Test
public void test_resolvable() {
    AnnotationConfigApplicationContext context =
            new AnnotationConfigApplicationContext(AppConfig.class);
    context.publishEvent(new ResolvableEntityCreatedEvent<>(new Order()));
    context.publishEvent(new ResolvableEntityCreatedEvent(new Person()));
    context.close();
}
// 结果符合预期
onOrderCreated Resolvable
onPersonCreated Resolvable

事件发布任意对象

原理分析

从Spring 4.2开始,事件基础结构得到了显著的改进,并提供了一个基于注释的模型,以及发布任何任意事件(也就是说,一个不一定是从ApplicationEvent扩展而来的对象)的能力。当这样一个对象被发布时会被包装在一个事件中。

调用 ApplicationEventPublisher#publishEvent(event) 发布事件时,如果event本身继承了 ApplicationEvent正常发送,如果 event 没有继承 ApplicationEvent 则将被包装成 PayloadApplicationEvent 来发送。

看下源码实现 AbstractApplicationContext#publishEvent(Object event, @Nullable ResolvableType eventType)

	protected void publishEvent(Object event, @Nullable ResolvableType eventType) {
		Assert.notNull(event, "Event must not be null");

		// Decorate event as an ApplicationEvent if necessary
		ApplicationEvent applicationEvent;
        // 1 本身实现了ApplicationEvent
		if (event instanceof ApplicationEvent) {
			applicationEvent = (ApplicationEvent) event;
		}
		else {
            // 本身没有实现 ApplicationEvent 包装成  PayloadApplicationEvent
			applicationEvent = new PayloadApplicationEvent<>(this, event);
			if (eventType == null) {
				eventType = ((PayloadApplicationEvent<?>) applicationEvent).getResolvableType();
			}
		}

		// Multicast right now if possible - or lazily once the multicaster is initialized
		if (this.earlyApplicationEvents != null) {
			this.earlyApplicationEvents.add(applicationEvent);
		}
		else {
            // 3 广播发送
			getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
		}

那么实现任意对象发布事件的关键是 PayloadApplicationEvent ,来看下其定义:

  • 实现了 ApplicationEvent 可作为事件发布
  • 泛型类,可包装任意事件内容
  • ResolvableTypeProvider 上面提到的类型解析接口,实现任意发布的关键!
public class PayloadApplicationEvent<T> extends ApplicationEvent implements ResolvableTypeProvider {}

使用案例

  1. 定义任意的发布对象,不继承 ApplicationEvent

    public class ArbitraryObject {
        private String name;
    
        public ArbitraryObject(String name) {
            this.name = name;
        }
    
        @Override
        public String toString() {
            return "ArbitraryObject{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
    
  2. 定义监听器监听 PayloadApplicationEvent 事件,指定泛型

    @Component
    public class PayloadApplicationEventListener {
    
        @EventListener
        public void onPayloadApplicationEvent(PayloadApplicationEvent<ArbitraryObject> event) {
    
            System.out.println("收到PayloadApplicationEvent: " + event.getPayload());
        }
    
    }
    
  3. 发送事件观察结果

    @Test
    public void test_arbitrary() {
        AnnotationConfigApplicationContext context =
                new AnnotationConfigApplicationContext(AppConfig.class);
        context.publishEvent(new ArbitraryObject("随意对象非ApplicationEvent事件"));
        context.close();
    }
    

    监听到的事件内容输出如下

    收到PayloadApplicationEvent: ArbitraryObject{name='随意对象非ApplicationEvent事件'}
    

总结

本文详解分析了Spring的事件发布原理,自定义事件可通过编程式或是注解方式。也介绍了泛型事件结构以及如何发送任意的事件对象。案例比较多,建议实践一下。

本篇源码地址: github.com/kongxubihai… 知识分享,转载请注明出处。学无先后,达者为先!