[版权申明] 非商业目的注明出处可自由转载
出自:shusheng007

概述

相信即使是刚入门Java的小朋友们对事件(Event)也不会陌生,只要接触过消息队列,例如RabbitMq、Kafka等的都清楚。其思想和观察者这个设计模式类似,不懂的点击链接先学习一下前置知识。

今天要谈论的Event是Spring给我们提供的一套类似的机制,某些场景下其有奇效,使你的技术水平立马绝尘于你的小伙伴们,装x成功是毫无疑问了…

设计思想

其思想就是观察者模式,一个Publisher发布Event,多个Listener去监听处理自己感兴趣的Event.

在这里插入图片描述

使用场景

那在日常使用Spring做开发的时候,都什么场景下适合使用这套事件机制呢?答案是:凡是需要解耦的场景均可以考虑。正常情况下我们程序的某个业务流程是一个调用链: 方法1 -> 方法2-> … 方法n, 这些方法还极有可能分布在不同的类里,这样这些类就会互相依赖,耦合在一起。当你不想让这种情况出现的时候,可以考虑事件了。

下面是几个比较合适的使用场景

日志

当某个操作需要记录日志时,可以考虑这种方式,将记录日志的具体实现与逻辑解耦。

通知

当有很多模块,例如类,依赖某个操作时,就可以使用事件将其解耦。

统计

例如要统计某个操作的执行次数等。

如何使用

这是我们的重点,但前面是前提,如果不了解前面的知识,这一部分也就变的本末倒置成了无根之木,无源之水了。

Spring4.2开始,Spring简化了Event的使用方式。这也是Spring一贯的作风,不断地隐藏事物的本质,暴露机械而傻瓜式的使用接口,把程序员变成码农,进而提高生产效率。码农只需要知道某个功能加个注解就可以了,至于为什么完全不用知道,这特别切合当前国内软件行业的生态,所以Spring在中国非常火。

让我们言归正传,由于我们现在使用的Spring版本基本都会高于Spring4.2,所以我们一般都会使用Event的新方式。

Event注解方式(Spring4.2后版本)

基本使用

事件三件套: publisher , event , listener。

  • publisher

Publisher由下面接口的实现类来承担,在springboot中ApplicationContext继承了这个接口,具体的实现类为AnnotationConfigServletWebServerApplicationContext, 但是这都不重要,重要的是我们只要声明注入这个接口的实现类Springboot就会给我们一个实例,然后我们就可以使用其发布事件了。

@FunctionalInterface
public interface ApplicationEventPublisher {
	default void publishEvent(ApplicationEvent event) {
		publishEvent((Object) event);
	}

	void publishEvent(Object event);
}

例如

@RequiredArgsConstructor
@Service
public class EventService {
    private final ApplicationEventPublisher publisher;

    public void goHome() {
        publisher.publishEvent(new OpenDoorEvent("open door", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)));
    }
}
  • Event

这个就简单了,任何类型都可以。例如我自定义一个开门的事件类

@RequiredArgsConstructor
@Getter
public class OpenDoorEvent {
    private final String event;
    private final String time;
}
  • Listenser

只要是Spring容器管理的Bean都可以充当Listenser。如下类通过@Component注解让Spring帮我们来管理其实例,然后我们只要在其内部的某个public方法上使用@EventListener标记即可。

@Component
public class HomeEventListener {

    @EventListener
    public void listenOpenDoorEvent(OpenDoorEvent event) {
        log.info("{}: {}", event.getTime(), event.getEvent());
    }

通过以上3步就完成了。当Publisher发布Event时,Listener会监听到并执行对应的逻辑。

高级使用

事件链

在前面的基础使用方法中,我们的Listener的返回值void,所以事件在此方法内处理完就结束了。但是如果给监听方法设置一个返回值的话,当这个监听方法执行完后就会继续将此返回值类型作为事件继续发布出去。

@EventListener
public TurnOnLightsEvent listenOpenDoorEvent(OpenDoorEvent event) {
    log.info("{}: {}", event.getTime(), event.getEvent());
    return new TurnOnLightsEvent("turn on light", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
}

上面的监听方法的返回值为TurnOnLightsEvent ,所以当其监听到OpenDoorEvent 事件并处理完后会继续发布一个TurnOnLightsEvent 事件。

设置监听事件类型

我们如何设置要监听哪个事件呢?有如下两种方法。

  • 方法参数

可以在监听方法里面指定一个事件参数(有且只有一个参数),如上面的代码所示。当然我们也可以指定一个基类,它的子类型事件也可以被接收到。

@EventListener
public void listenContextEvents(ApplicationContextEvent event) {
    log.info("context event received: {}", event.getClass().getSimpleName());
}

例如,当启动StringBoot程序时,输出如下日志:

 [  restartedMain] t.s.c.springevent.HomeEventListener  : context event received: ContextRefreshedEvent

可见其监听到了ApplicationContextEvent 类型的子类ContextRefreshedEvent

  • @EventListener 注解

到目前为止,我们一直没有给给@EventListener指定任何参数,其实它有好几个参数,其中一个就与事件类型相关,如下所示。

...
public @interface EventListener {
    ...
	@AliasFor("value")
	Class<?>[] classes() default {};
	...
}

我们可以通过这个属性来指定要监听的事件集合。当通过注解设置了要监听事件集合后,方法里面可以不声明任何参数。如果要声明的话只能声明一个参数,且这个参数必须是设置的那些事件类型的父类。例如下面的代码,我声明了一个object类型。

通过正常参数,我们才可以取到事件的信息。

@EventListener(classes = {OpenDoorEvent.class, TurnOnLightsEvent.class})
public void listenMultiEvent(Object object) {
    log.info("监听到了事件:{}",object);
}

设置监听条件

我们还可以设置监听条件,也就是设置在什么情况下触发监听方法。@EventListener使用一个参数用来实现这个功能。这个condition需要一个SpEL,不知道啥是SpEL的小朋友面壁3分钟。

...
public @interface EventListener {
    ...
	String condition() default "";
	...
}

当不设置时,其值是默认的"", 表示任何条件都执行。如果设置了这个条件的话,要求你设置的SpEL表达式的值必须为true或者是下面这些字符串 “true”, “on”, “yes”, "1"之一。

下面的示例表示只处理事件名称为 turn on light的事件。

@EventListener(classes = {OpenDoorEvent.class, TurnOnLightsEvent.class},
        condition = "#object.event eq 'turn on light'")
public void listenMultiEvent(Object object) {
    log.info("监听到了事件:{}", object);
}

输出

监听到了事件:top.shusheng007.composite.springevent.event.TurnOnLightsEvent@1963dcc7

SpEL中,我们可以使用下面的方式获取相关值

  • #root.event or event for references to the ApplicationEvent
  • #root.args or args for references to the method arguments array
  • Method arguments can be accessed by index. For example, the first argument can be accessed via #root.args[0], args[0], #a0, or #p0.
  • Method arguments can be accessed by name (with a preceding hash tag) if parameter names are available in the compiled byte code.

我的例子采用了第4种方式,直接通过#参数名称获取了方法参数。我们也可以使用#root.args[0]获取,小朋友们可以试一下

异步执行

默认情况下,监听方法与发布方法是同步执行的,都在同一条线程里。如果监听方法阻塞或者发生了异常,会影响到发布方法。

但是很多时候我们希望发布者在发布了事件后不受监听者的影响,此时就需要采用异步监听处理的方式。

  • 使程序支持异步

使用@EnableAsync标记任何一个Configuration类,或者你的SpringBootApplication。自定义线程池,这一步是可选的,因为不自定的话springboot将使用默认线程池,但是一般实践当中都是要自定义的 。

@Slf4j
@EnableAsync
@Configuration
public class ConcurrencyConfig {

    @Primary
    @Bean
    public TaskExecutor threadPoolExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(8);
        executor.setQueueCapacity(20);
        executor.setKeepAliveSeconds(1);
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setThreadNamePrefix("task-thread-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());

        executor.initialize();
        return executor;
    }
}

对自定义线程池有疑问的可以参考:秒懂SpringBoot之@Async如何自定义线程池

  • 使用@Aync标记监听方法
    @Async
    @EventListener
    public void listenCloseCurtainEvent(CloseCurtainEvent event) {
        log.info("{}: {}", event.getTime(), event.getEvent());
    }

输出

2024-01-28 22:31:28.878  INFO 2656 --- [nio-8080-exec-1] t.s.c.springevent.EventController        : start event
2024-01-28 22:31:28.881  INFO 2656 --- [nio-8080-exec-1] t.s.c.springevent.HomeEventListener      : 2024-01-28T22:31:28.8781349: open door
2024-01-28 22:31:28.882  INFO 2656 --- [nio-8080-exec-1] t.s.c.springevent.HomeEventListener      : 2024-01-28T22:31:28.8811352: turn on light
2024-01-28 22:31:28.889  INFO 2656 --- [  task-thread-1] t.s.c.springevent.HomeEventListener      : 2024-01-28T22:31:28.8821354: close the curtain
2024-01-28 22:31:28.907  INFO 2656 --- [nio-8080-exec-1] t.s.c.springevent.EventController        : end event

从输出日志可以看到,Publisher 运行在nio-8080-exec-1线程上,而监听方法listenCloseCurtainEvent 运行在 task-thread-1 线程上。

事务事件

Spring还提供了一个注解 TransactionalEventListener 用来将事件监听集成到事务流程中。

其与Spring Data Jpa结合起来使用也是很有意义的方式。

事件顺序

默认情况下,同一个事件被多个Listener监听的时候,其是没有顺序的,也就是说不能保证哪个Listener先执行,哪个后执行。

如果想让其有序执行,需要使用@Orger注解来标记相应的Listener类或者方法,如下所示:

@Order(1)
@EventListener(classes = {OpenDoorEvent.class, TurnOnLightsEvent.class})
public void listenMultiEvent(Object object) {
    log.info("监听到了事件:{}", object);
}

@Order(2)
@EventListener
public CloseCurtainEvent listenTurnOnLightEvent(TurnOnLightsEvent event) {
    log.info("{}: {}", event.getTime(), event.getEvent());
    return new CloseCurtainEvent("close the curtain",
            LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
}

上面两个监听器都会监听TurnOnLightsEvent 事件,但是listenMultiEvent会先于listenTurnOnLightEvent执行,因为我们加了@Order

泛型事件

个人觉得这玩意儿意义不大,我们可以简单的聊两句。

首先定义一个泛型事件,如下所示:

/**
 * Copyright (C) 2023 ShuSheng007
 * <p>
 * Author ShuSheng007
 * Time 2024/1/28 23:18
 * Description
 */
public class GenericDailyEvent<T> extends ApplicationEvent implements ResolvableTypeProvider {
    private T myEvent;

    public GenericDailyEvent(Object source,T myEvent) {
        super(source);
        this.myEvent = myEvent;
    }

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

    public T getMyEvent() {
        return myEvent;
    }
}

然后发布并监听泛型事件。

@EventListener
public void listenGenericEvent(GenericDailyEvent<FinishPoopEvent> event){
    log.info("{}: {}", event.getMyEvent().getTime(), event.getMyEvent().getEvent());
}

技术原理

实现原理为:Spring 通过EventListenerMethodProcessor@EventListener 包装成ApplicationListener, 具体为ApplicationListenerMethodAdapter, 注册到Spring系统中。

public class EventListenerMethodProcessor
		implements SmartInitializingSingleton, ApplicationContextAware, BeanFactoryPostProcessor {

	private void processBean(final String beanName, final Class<?> targetType) {
	    ...
	   	// Non-empty set of methods
		ConfigurableApplicationContext context = this.applicationContext;
		Assert.state(context != null, "No ApplicationContext set");
		List<EventListenerFactory> factories = this.eventListenerFactories;
		Assert.state(factories != null, "EventListenerFactory List not initialized");
		for (Method method : annotatedMethods.keySet()) {
			for (EventListenerFactory factory : factories) {
				if (factory.supportsMethod(method)) {
					Method methodToUse = AopUtils.selectInvocableMethod(method, context.getType(beanName));
					ApplicationListener<?> applicationListener =
							factory.createApplicationListener(beanName, targetType, methodToUse);
					if (applicationListener instanceof ApplicationListenerMethodAdapter) {
						((ApplicationListenerMethodAdapter) applicationListener).init(context, this.evaluator);
					}
					context.addApplicationListener(applicationListener);
					break;
				}
			}
		}
	}
}

总结

关于Spring Framwork 提供的事件机制就聊到这里了,其在日常开发过程中出境率还是挺高的,希望对后辈有所帮助。

源码

一如既往,你可以点击本文首发,从文末找到源码。

Logo

Kafka开源项目指南提供详尽教程,助开发者掌握其架构、配置和使用,实现高效数据流管理和实时处理。它高性能、可扩展,适合日志收集和实时数据处理,通过持久化保障数据安全,是企业大数据生态系统的核心。

更多推荐