Spring高手之路7——事件机制与监听器的全面探索

news2024/11/26 16:53:04

文章目录

  • 1. Spring中的观察者模式
  • 2. 监听器
    • 2.1 实现ApplicationListener接口创建监听器
    • 2.2 @EventListener注解创建监听器
    • 2.3 对比ApplicationListener接口和@EventListener注解的创建方式
  • 3. Spring的事件机制
    • 3.1 ApplicationEvent
    • 3.2 ApplicationContextEvent
    • 3.3 ContextRefreshedEvent 和 ContextClosedEvent
    • 3.4 ContextStartedEvent 和 ContextStoppedEvent
  • 4. 自定义事件开发
    • 4.1 注解式监听器和接口式监听器对比触发时机
    • 4.2 @Order注解调整监听器的触发顺序

1. Spring中的观察者模式

  观察者模式是一种行为设计模式,它定义了对象之间的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并被自动更新。在这个模式中,改变状态的对象被称为主题,依赖的对象被称为观察者。

举个实际的例子:

  1. 事件源(Event Source):可以视为“主题(Subject)”,当其状态发生变化时(比如播放新的内容),会通知所有的观察者。想象我们正在听广播,广播电台就是一个事件源,它提供了大量的新闻、音乐和其他内容。

  2. 事件(Event):这是主题状态改变的具体表示,对应到广播例子中,就是新闻、音乐和其他内容。每当电台播放新的内容时,就相当于一个新的事件被发布了。

  3. 广播器(Event Publisher / Multicaster):广播器起到的是中介的作用,它将事件从事件源传递到监听器。在这个例子中,广播塔就充当了这个角色,它将电台的节目的无线电信号发送到空气中,以便无线电接收器(监听器)可以接收。

  4. 监听器(Listener):监听器就是“观察者”,它们监听并响应特定的事件。在例子中,无线电接收器就是监听器,它接收广播塔发出的信号,然后播放电台的内容。

Spring中,事件模型的工作方式也是类似的:

  1. Spring应用程序中发生某个行为时(比如一个用户完成了注册),那么产生这个行为的组件(比如用户服务)就会创建一个事件,并将它发布出去。
  2. 事件一旦被发布,SpringApplicationContext就会作为广播器,把这个事件发送给所有注册的监听器。
  3. 各个监听器接收到事件后,就会根据事件的类型和内容,进行相应的处理(比如发送欢迎邮件,赠送新用户优惠券等)。

  这就是Spring事件模型的工作原理,它实现了事件源、广播器和监听器之间的解耦,使得事件的生产者和消费者可以独立地进行开发和修改,增强了程序的灵活性和可维护性。


2. 监听器

2.1 实现ApplicationListener接口创建监听器

  首先,我们创建一个监听器。在Spring框架中,内置的监听器接口是ApplicationListener,这个接口带有一个泛型参数,代表要监听的具体事件。我们可以通过实现这个接口来创建自定义的监听器。

  当所有的Bean都已经被初始化后,Spring会发布一个ContextRefreshedEvent事件,告知所有的监听器,应用上下文已经准备好了,我们可以创建一个监听器来监听ContextRefreshedEvent事件

全部代码如下:

package com.example.demo.listener;

import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;

@Component
public class MyContextRefreshedListener implements ApplicationListener<ContextRefreshedEvent> {
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        System.out.println("MyContextRefreshedListener收到ContextRefreshedEvent事件!");
    }
}

注意,我们使用@Component注解来标记这个监听器,这样在Spring进行包扫描的时候能够找到并自动注册它。

接下来,我们需要创建一个启动类来启动IOC容器并测试这个监听器。

主程序:

package com.example.demo;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;


public class DemoApplication {

    public static void main(String[] args) {
        System.out.println("开始初始化IOC容器...");
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext("com.example.demo.listener");
        System.out.println("IOC容器初始化完成...");
        ctx.close();
        System.out.println("IOC容器已关闭...");
    }

}

运行这个程序会看到控制台上输出了在MyContextRefreshedListener中打印的消息,表明监听器成功接收到了事件。

运行结果:

在这里插入图片描述

2.2 @EventListener注解创建监听器

  除了通过实现ApplicationListener接口来创建监听器,我们还可以通过注解来创建。Spring提供了@EventListener注解,我们可以在任何一个方法上使用这个注解来指定这个方法应该在收到某种事件时被调用。

  比如,我们可以创建一个新的监听器来监听ContextClosedEvent事件,这个事件代表Spring的应用上下文即将关闭:

增加一个MyContextClosedListener类,方便和前面接口创建监听器进行对比

@Component
public class MyContextClosedListener {
    @EventListener
    public void handleContextClosedEvent(ContextClosedEvent event) {
        System.out.println("MyContextClosedListener收到ContextClosedEvent事件!");
    }
}

运行结果:

在这里插入图片描述

  ContextClosedEvent事件是在Spring应用上下文被关闭时发布的,这通常在所有的单例Bean已经被销毁之后。通过监听这个事件,我们可以在应用上下文关闭时执行一些清理工作。

2.3 对比ApplicationListener接口和@EventListener注解的创建方式

  1. 使用ApplicationListener接口:

上面说过,由于ApplicationListener接口是泛型接口,这个接口带有一个泛型参数,代表要监听的具体事件。

创建ContextRefreshedEvent事件的监听器:

import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;

@Component
public class ContextRefreshedListener implements ApplicationListener<ContextRefreshedEvent> {

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        System.out.println("Received ContextRefreshedEvent - Context refreshed!");
    }
}

创建ContextClosedEvent事件的监听器:

import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.stereotype.Component;

@Component
public class ContextClosedListener implements ApplicationListener<ContextClosedEvent> {

    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        System.out.println("Received ContextClosedEvent - Context closed!");
    }
}
  1. 使用@EventListener注解:
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class ApplicationContextEventListener {

    @EventListener
    public void handleContextRefreshedEvent(ContextRefreshedEvent event) {
        System.out.println("Received ContextRefreshedEvent - Context refreshed!");
    }

    @EventListener
    public void handleContextClosedEvent(ContextClosedEvent event) {
        System.out.println("Received ContextClosedEvent - Context closed!");
    }
}

  在上述代码中,我们使用@EventListener注解定义了两个方法,分别处理ContextRefreshedEventContextClosedEvent事件。不论何时ContextRefreshedEventContextClosedEvent被发布,相应的监听器就会被触发,然后在控制台打印出相应的信息。

  • ContextRefreshedEvent 事件在 Spring 容器初始化或者刷新时触发,此时所有的 Bean 都已经被完全加载,且 post-processor 也已经被调用,但此时容器尚未开始接收请求。

  • ContextClosedEvent 事件在 Spring 容器关闭时触发,此时容器尚未销毁所有 Bean。当接收到这个事件后可以做一些清理工作。


3. Spring的事件机制

  在 Spring 中,事件(Event)和监听器(Listener)是两个核心概念,它们共同构成了 Spring 的事件机制。这一机制使得在 Spring 应用中,组件之间可以通过发布和监听事件来进行解耦的交互。

Spring中有4个默认的内置事件

  • ApplicationEvent
  • ApplicationContextEvent
  • ContextRefreshedEventContextClosedEvent
  • ContextStartedEventContextStoppedEvent

我们分别来看一下

3.1 ApplicationEvent

  在 Spring 中,ApplicationEvent 是所有事件模型的抽象基类,它继承自 Java 原生的 EventObjectApplicationEvent 本身是一个抽象类,它并未定义特殊的方法或属性,只包含事件发生时的时间戳,这意味着我们可以通过继承ApplicationEvent来创建自定义的事件。

  比如我们希望创建自定义事件,可以直接继承 ApplicationEvent 类。

完整代码如下:

创建一个 CustomApplicationEvent

package com.example.demo.event;

import org.springframework.context.ApplicationEvent;

public class CustomApplicationEvent extends ApplicationEvent {
    private String message;

    public CustomApplicationEvent(Object source, String message) {
        super(source);
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

然后创建一个监听器来监听这个自定义事件:

package com.example.demo.listener;

import com.example.demo.event.CustomApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Component
public class CustomApplicationEventListener implements ApplicationListener<CustomApplicationEvent> {

    @Override
    public void onApplicationEvent(CustomApplicationEvent event) {
        System.out.println("Received custom event - " + event.getMessage());
    }
}

最后,在应用中的某个地方发布这个自定义事件:

package com.example.demo.event;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;

@Component
public class CustomEventPublisher {
    private final ApplicationEventPublisher publisher;

    public CustomEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void doSomethingAndPublishAnEvent(final String message) {
        System.out.println("Publishing custom event.");
        CustomApplicationEvent customApplicationEvent = new CustomApplicationEvent(this, message);
        publisher.publishEvent(customApplicationEvent);
    }
}

  当调用 doSomethingAndPublishAnEvent() 方法时,CustomApplicationEventListener 就会收到 CustomApplicationEvent 并打印出自定义消息,这就是通过继承ApplicationEvent自定义事件并进行监听的一种方式。

主程序:

package com.example.demo;

import com.example.demo.event.CustomEventPublisher;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;


public class DemoApplication {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("com.example.demo");
        CustomEventPublisher publisher = context.getBean(CustomEventPublisher.class);
        publisher.doSomethingAndPublishAnEvent("hello world");
    }
}

  有人可能会疑惑,context.getBean(CustomEventPublisher.class)为什么不报错呢?CustomEventPublisher只有一个带参数的构造方法啊。

  从Spring 4.3开始,如果类只有一个构造方法,那么Spring将会自动把这个构造方法当作是我们希望进行自动装配的构造方法,无需显式地添加@Autowired@inject注解。如果类有多个构造方法,并且没有在任何构造方法上使用@Autowired@inject注解,那么Spring将会使用无参数的构造方法(如果存在的话)来创建这个类的实例。Spring会尝试在已经创建的bean中寻找能够满足构造器参数要求的bean,并自动将这些bean注入到构造方法中,这就是所谓的自动装配。

  在这个例子中,CustomEventPublisher这个类只有一个带有ApplicationEventPublisher参数的构造方法。Spring在创建CustomEventPublisher的实例时,会尝试寻找一个已经创建的ApplicationEventPublisher类型的bean来满足这个构造方法的参数要求。

  而ApplicationEventPublisherSpring内置的一个接口,对应的实例是在Spring容器启动时就已经被Spring自动创建好的,因此Spring能够找到一个ApplicationEventPublisher类型的bean,然后将这个bean注入到CustomEventPublisher的构造方法中,这样就能够成功创建CustomEventPublisher的实例。

  所以,即使CustomEventPublisher这个类没有无参构造器,Spring也可以通过自动装配功能成功地创建这个类的实例。

运行结果:

在这里插入图片描述

3.2 ApplicationContextEvent

  ApplicationContextEventApplicationEvent 的子类,它代表了与 Spring 应用上下文(ApplicationContext)有关的事件。这个抽象类在构造方法中接收一个 ApplicationContext 对象作为事件源(source)。这意味着在事件触发时,我们可以通过事件对象直接获取到发生事件的应用上下文,而不需要进行额外的操作。

在这里插入图片描述

  Spring 内置了一些事件,如 ContextRefreshedEventContextClosedEvent,这些都是 ApplicationContextEvent 的子类。ApplicationContextEventApplicationEvent 的子类,专门用来表示与Spring应用上下文相关的事件。虽然 ApplicationContextEvent 是一个抽象类,但在实际使用时,通常会使用其具体子类,如 ContextRefreshedEventContextClosedEvent,而不是直接使用 ApplicationContextEvent。另外,虽然我们可以创建自定义的 ApplicationContextEvent 子类或 ApplicationEvent 子类来表示特定的事件,但这种情况比较少见,因为大多数情况下,Spring内置的事件类已经能满足我们的需求。

3.3 ContextRefreshedEvent 和 ContextClosedEvent

  ContextRefreshedEvent 事件在 Spring 容器初始化或者刷新时触发,此时所有的 Bean 都已经被完全加载,且 post-processor 也已经被调用,但此时容器尚未开始接收请求。

  ContextClosedEvent 事件在 Spring 容器关闭时触发,此时容器尚未销毁所有 Bean。当接收到这个事件后可以做一些清理工作。

  这里我们再次演示实现接口来创建监听器,不过和2.3节不同,我们只创建的1个类,同时处理ContextRefreshedEventContextClosedEvent事件。这里实现ApplicationListener接口,泛型参数使用ContextRefreshedEventContextClosedEvent的父类ApplicationEvent

  在Spring中创建一个类来监听多个事件,然后在onApplicationEvent方法中检查事件的类型。

全部代码如下:

package com.example.demo.listener;

import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;

@Component
public class MyApplicationContextEventListener implements ApplicationListener<ApplicationEvent> {

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof ContextRefreshedEvent) {
            System.out.println("Context Refreshed Event received.");
            // Handle the event
        } else if (event instanceof ContextClosedEvent) {
            System.out.println("Context Closed Event received.");
            // Handle the event
        }
    }
}

主程序:

package com.example.demo;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;


public class DemoApplication {
    public static void main(String[] args) {
        System.out.println("开始初始化IOC容器...");
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("com.example.demo");
        System.out.println("IOC容器初始化完成...");
        context.close();
        System.out.println("IOC容器已关闭...");
    }
}

运行结果:

在这里插入图片描述

  在这个例子中,MyApplicationContextEventListener现在只实现了ApplicationListener<ApplicationEvent>接口。然后在onApplicationEvent方法中,我们检查事件的类型,并根据事件的类型执行相应的操作。这样我们就可以在同一个监听器中处理多种类型的事件了。

3.4 ContextStartedEvent 和 ContextStoppedEvent

  • ContextStartedEvent:这是Spring应用上下文的启动事件。当调用实现了 Lifecycle 接口的 Beanstart 方法时,Spring会发布这个事件。这个事件的发布标志着Spring应用上下文已经启动完成,所有的Bean都已经被初始化并准备好接收请求。我们可以监听这个事件来在应用上下文启动后执行一些自定义逻辑,比如开启一个新线程,或者连接到一个远程服务等。

  • ContextStoppedEvent:这是Spring应用上下文的停止事件。当调用实现了 Lifecycle 接口的 Beanstop 方法时,Spring会发布这个事件。这个事件的发布标志着Spring应用上下文开始停止的过程,此时Spring将停止接收新的请求,并开始销毁所有的Singleton Bean。我们可以监听这个事件来在应用上下文停止前执行一些清理逻辑,比如关闭数据库连接,释放资源等。

  有人可能会疑问了,实现了 Lifecycle 接口的 Beanstart 方法和stop方法是什么?这与@PostConstruct@PreDestroy有什么区别?

  Lifecycle 接口有startstop2个方法,start() 方法将在所有 Bean 都已被初始化后,整个应用上下文启动时被调用,而 stop() 方法将在应用上下文关闭,但是在所有单例 Bean 被销毁之前被调用。这意味着这些方法将影响整个应用上下文的生命周期。

  总的来说,@PostConstruct@PreDestroy 主要关注单个 Bean 的生命周期,而 Lifecycle 接口则关注整个应用上下文的生命周期。

  言归正传,回到这小节的主题,当 Spring 容器接收到 ContextStoppedEvent 事件时,它会停止所有的单例 Bean,并释放相关资源。

全部代码如下:

package com.example.demo.listener;

import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextStartedEvent;
import org.springframework.context.event.ContextStoppedEvent;
import org.springframework.stereotype.Component;

@Component
public class MyContextStartStopEventListener implements ApplicationListener<ApplicationEvent> {

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof ContextStartedEvent) {
            System.out.println("Context Started Event received.");
            // Handle the event
        } else if (event instanceof ContextStoppedEvent) {
            System.out.println("Context Stopped Event received.");
            // Handle the event
        }
    }
}
package com.example.demo;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;


public class DemoApplication {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("com.example.demo");
        // 触发 ContextStartedEvent
        context.start();
        // 触发 ContextStoppedEvent
        context.stop();
    }
}

运行结果:

在这里插入图片描述

  对于ContextStartedEventContextStoppedEvent事件来说,需要手动调用context.start()context.stop()来触发这两个事件。ContextStartedEvent事件是在ApplicationContext启动并且所有单例bean在完全初始化后被发布的,而ContextStoppedEvent事件是在ApplicationContext停止时发布的。

  为什么这里context.start()context.stop()能触发ContextStartedEvent事件和ContextStoppedEvent事件呢?

  我们来看看源码,在 Spring 中,AnnotationConfigApplicationContext 并没有直接实现 Lifecycle 接口。

在这里插入图片描述

  但是从图中我们可以看到,AnnotationConfigApplicationContext 继承自 GenericApplicationContextGenericApplicationContext继承自 AbstractApplicationContext抽象类。AbstractApplicationContext 类实现了 ConfigurableApplicationContext 接口,这个ConfigurableApplicationContext接口继承了 Lifecycle 接口。

  所以,从类的继承层次上来看,AnnotationConfigApplicationContext 是间接实现了 Lifecycle 接口的。当我们在 AnnotationConfigApplicationContext 的对象上调用 start()stop() 方法时,它就会触发相应的 ContextStartedEventContextStoppedEvent 事件。

  在实际的Spring应用中,很少需要手动调用start()stop()方法。这是因为Spring框架已经为我们处理了大部分的生命周期控制,比如bean的创建和销毁,容器的初始化和关闭等。


4. 自定义事件开发

4.1 注解式监听器和接口式监听器对比触发时机

需求背景:假设正在开发一个论坛应用,当新用户成功注册后,系统需要进行一系列的操作。这些操作包括:

  1. 向用户发送一条确认短信;
  2. 向用户的电子邮箱发送一封确认邮件;
  3. 向用户的站内消息中心发送一条确认信息;

  为了实现这些操作,我们可以利用Spring的事件驱动模型。具体来说,当用户注册成功后,我们可以发布一个“用户注册成功”的事件。这个事件将包含新注册用户的信息。

  然后,我们可以创建多个监听器来监听这个“用户注册成功”的事件。这些监听器分别对应于上述的三个操作。当监听器监听到“用户注册成功”的事件后,它们将根据事件中的用户信息,执行各自的操作。

  这里为了对比,会把两种监听器的创建方式一起使用,实际开发中只需要使用某一种方式即可。

全部代码如下:

首先,让我们定义事件:

package com.example.demo.event;

import org.springframework.context.ApplicationEvent;

public class UserRegistrationEvent extends ApplicationEvent {
    private String username;

    public UserRegistrationEvent(Object source, String username) {
        super(source);
        this.username = username;
    }

    public String getUsername() {
        return username;
    }
}

接下来,让我们定义一个接口式监听器来发送短信通知:

package com.example.demo.listener;

import com.example.demo.event.UserRegistrationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Component
public class SmsNotificationListener implements ApplicationListener<UserRegistrationEvent> {
    @Override
    public void onApplicationEvent(UserRegistrationEvent event) {
        System.out.println("发送短信通知,恭喜用户 " + event.getUsername() + " 注册成功!");
    }
}

我们也可以定义一个注解式监听器来发送邮件通知:

package com.example.demo.listener;

import com.example.demo.event.UserRegistrationEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class EmailNotificationListener {
    @EventListener
    public void handleUserRegistrationEvent(UserRegistrationEvent event) {
        System.out.println("发送邮件通知,恭喜用户 " + event.getUsername() + " 注册成功!");
    }
}

以及一个注解式监听器来发送站内信通知:

package com.example.demo.listener;

import com.example.demo.event.UserRegistrationEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class InternalMessageNotificationListener {
    @EventListener
    public void handleUserRegistrationEvent(UserRegistrationEvent event) {
        System.out.println("发送站内信通知,恭喜用户 " + event.getUsername() + " 注册成功!");
    }
}

最后,我们需要一个发布事件的方法,在用户注册成功后调用。我们可以在注册服务中添加这个方法:

package com.example.demo.service;

import com.example.demo.event.UserRegistrationEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;

@Service
public class UserRegistrationService {
    @Autowired
    private ApplicationContext applicationContext;

    public void registerUser(String username) {
        // 用户注册逻辑......

        // 用户注册成功,发布事件
        UserRegistrationEvent event = new UserRegistrationEvent(this, username);
        applicationContext.publishEvent(event);
    }
}

以上就是使用两种不同监听器的示例。如果你运行这个代码,你会发现,注解式监听器(邮件通知和站内信通知)的触发时机是在接口式监听器(短信通知)之前。

如果不使用SpringBoot,我们可以使用Spring的传统应用上下文初始化和启动应用

主程序如下:

package com.example.demo;

import com.example.demo.service.UserRegistrationService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;


public class DemoApplication {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("com.example.demo");
        UserRegistrationService userRegistrationService = context.getBean(UserRegistrationService.class);
        userRegistrationService.registerUser("testUser");
        context.close();
    }
}

从应用上下文中获取UserRegistrationServiceBean,调用registerUser方法触发事件。

运行结果:

在这里插入图片描述

从这里也可以得出一个结论:注解式监听器的触发时机比接口式监听器早

如果使用SpringBoot框架的主程序:

package com.example.demo;

import com.example.demo.service.UserRegistrationService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;


@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @Bean
    public CommandLineRunner commandLineRunner(UserRegistrationService userRegistrationService) {
        return args -> {
            userRegistrationService.registerUser("testUser");
        };
    }
}

运行结果:

在这里插入图片描述

  在这个示例中,我们创建了一个SpringBoot应用。通过注入UserRegistrationService并调用其registerUser方法,我们可以触发用户注册事件。在实际的应用中可能会在一个控制器或其他服务中调用这个服务。

  前一篇生命周期的顺序中,我们提到了初始化Bean的时候属性赋值、@PostConstruct注解、实现InitializingBean接口后的afterPropertiesSet方法和init-method指定的方法。那这里注解式监听器的顺序和这些生命周期的顺序又有什么关系呢?

对于监听器:

  • 对于使用@EventListener注解的监听器:它的触发是在ApplicationContext刷新完成之后,此时所有的Bean定义都已经加载,且完成了自动装配,也就是说,已经完成了构造器的调用和setter方法的调用,但是还未执行初始化回调(如@PostConstructInitializingBean接口的afterPropertiesSet方法或指定的init-method

  • 对于实现了ApplicationListener接口的监听器:它本身作为一个Bean,会经历常规的Bean生命周期阶段,包括构造器的调用、setter方法的调用以及初始化回调。然后在所有单例Bean的初始化完成后,也就是在ContextRefreshedEvent事件发出后,这些监听器才会被触发。

  所以总的来说,使用@EventListener的监听器会比Bean的初始化方法更早得到执行,而实现ApplicationListener接口的监听器在所有单例Bean初始化完毕后才会被触发。

4.2 @Order注解调整监听器的触发顺序

  刚刚的例子中,因为发送短信的监听是接口式的,而注解式监听器的触发时机比接口式监听器早,所以一直在会后才触发。这里我们利用@Order注解来强制改变一下触发顺序。

  @Order注解可以用在类或者方法上,它接受一个整数值作为参数,这个参数代表了所注解的类或者方法的“优先级”。数值越小,优先级越高,越早被调用。@Order的数值可以为负数,在int范围之内都可以。

  假设我们希望短信通知的优先级最高,其次是站内信通知,最后才是邮件通知。那么我们可以如下使用@Order注解

那么,我们修改一下监听器的顺序

package com.example.demo.listener;

import com.example.demo.event.UserRegistrationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Component
@Order(1)
public class SmsNotificationListener implements ApplicationListener<UserRegistrationEvent> {
    @Override
    public void onApplicationEvent(UserRegistrationEvent event) {
        System.out.println("发送短信通知,恭喜用户 " + event.getUsername() + " 注册成功!");
    }
}
package com.example.demo.listener;

import com.example.demo.event.UserRegistrationEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Component
public class InternalMessageNotificationListener {
    @EventListener
    @Order(2)
    public void handleUserRegistrationEvent(UserRegistrationEvent event) {
        System.out.println("发送站内信通知,恭喜用户 " + event.getUsername() + " 注册成功!");
    }
}
package com.example.demo.listener;

import com.example.demo.event.UserRegistrationEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Component
public class EmailNotificationListener {
    @EventListener
    @Order(3)
    public void handleUserRegistrationEvent(UserRegistrationEvent event) {
        System.out.println("发送邮件通知,恭喜用户 " + event.getUsername() + " 注册成功!");
    }
}

  可能是因为版本原因,经过我的测试,如果是注解式创建的监听器,@Order写在类上面会让所有的监听器的顺序控制失效。所以,接口式监听器如果要加@Order就放在类上,注解式监听器的@Order就放在方法上。

运行结果:

在这里插入图片描述

  1. 对于实现ApplicationListener接口的监听器(即接口式监听器),如果不指定@Order,它的执行顺序通常在所有指定了@Order的监听器之后。这是因为Spring默认给未指定@Order的监听器赋予了LOWEST_PRECEDENCE的优先级。

  2. 对于使用@EventListener注解的方法(即注解式监听器),如果不显式指定@Order,那么它的执行顺序就默认指定为@Order(0)

  注意:我们应该减少对事件处理顺序的依赖,以便更好地解耦我们的代码。虽然 @Order 可以指定监听器的执行顺序,但它不能改变事件发布的异步性质。@Order注解只能保证监听器的调用顺序,事件监听器的调用可能会在多个线程中并发执行,这样就无法保证顺序,而且在分布式应用也不适用,无法在多个应用上下文环境保证顺序。



欢迎一键三连~

有问题请留言,大家一起探讨学习

----------------------Talk is cheap, show me the code-----------------------

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/701125.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于weka平台手工实现(LinearRegression | Ridge Regression,岭回归)

一、普通的线性回归 线性回归主要采用最小二乘法来实现&#xff0c;主要思想如下&#xff1a; X ( x 11 x 12 ⋯ x 1 d 1 x 21 x 22 ⋯ 5 1 ⋮ ⋮ ⋱ ⋮ ⋮ x m 1 x m 2 ⋯ x m d 1 ) X\left( \begin{matrix} x_{11} & x_{12} & \cdots & x_{1d} & 1 \\ x_{2…

Vinted店铺为什么被封?如何应对?

Vinted是一家在线二手交易平台&#xff0c;专门用于买卖衣物和时尚配件。自从2022年以来&#xff0c;Vinted也越来越向综合性跨境电商平台转变。细心的伙伴都会发现&#xff0c;近来Vinted这阵子封号确实很严重&#xff0c;感觉是风控变严格了&#xff0c;但是万变不离其宗&…

xhtmlrenderer 将html转换成pdf,设置多字体, 以及中文不显示的问题

接上一篇 https://blog.csdn.net/qq_21480147/article/details/131187202 多字体 字体文件自行搜索或者window中自带的搜索(C:\Windows\Fonts) 中文不显示 在要渲染的中文的地方中设置stylefont-family:[字体] 该字体需要对应指定的属性, 属性参考:

java程序改变io临时存储路径

System.setProperty(“java.io.temdir”,“your path”)

【UE5 Cesium】08-Cesium for Unreal 子关卡应用实例(上)

UE版本&#xff1a;5.1 效果 &#xff08;运行游戏可以看到进入关卡体积内楼房模型才会显现&#xff0c;以此来减少电脑性能消耗&#xff09; 步骤 一、新建两个子关卡&#xff08;以北京和上海为例&#xff09; 点击窗口-》关卡-》新建 命名第一个子关卡为“SubLevel_Bei…

计算机专业学生暑假要去看这些经典书籍!

好书在精不在多&#xff0c;每一本经典书籍都值得反复咀嚼&#xff0c;温故而知新&#xff01; 分享几本经典书籍。 重构 改善既有代码的设计 就像豆瓣评论所说的&#xff0c;看后有种醍醐灌顶、欲罢不能的感觉。无论你是初学者&#xff0c;还是深耕多年的老手&#xff0c;这…

Bytebase VS Yearning

下文对 Bytebase 和 Yearning 两个数据库管理工具进行了多维度比较&#x1f50d;。 产品功能定位 Yearning&#xff1a;功能较为单一的独立数据库审核工具&#xff0c;适合小团队进行简单的 SQL 审核&#xff0c;若要应对复杂需求必须进行大量二次开发&#xff0c;用户群更偏…

从功能测试进阶自动化测试,熬夜7天整理出这一份3000字超全学习指南!

因为我一直在分享自动化测试技术&#xff0c;所以&#xff0c;经常被问到&#xff1a; 功能测试想转自动化&#xff0c;请问应该怎么入手&#xff1f;或者有哪些书推荐&#xff1f; 那么&#xff0c;接下来我就结合自己的经历聊一聊我是如何在工作中做自动化测试的。 测试新…

【技术操作】EasyCVR如何在分享页增加控制台跳转?

EasyCVR可拓展性强、视频能力灵活、部署轻快&#xff0c;可支持的主流标准协议有GB28181、RTSP/Onvif、RTMP等&#xff0c;以及厂家私有协议与SDK接入&#xff0c;包括海康Ehome、海大宇等设备的SDK等&#xff0c;能对外分发RTSP、RTMP、FLV、HLS、WebRTC等格式的视频流。 在Ea…

6.4.2 文件隐藏属性

chattr指令只能在Ext2/Ext3/Ext4的 Linux 传统文件系统上面完整生效&#xff0c; 其他的文件系统可能就无法完整的支持这个指令了&#xff0c;例如 xfs 仅支持部份参数而已。 chattr &#xff08;设置文件隐藏属性&#xff09; 这个指令很重要&#xff0c;在系统的数据安全上面…

互联网医院平台|互联网医院搭建|线上医疗系统开发必要功能

医疗服务行业一直以来都有着较好的发展市场&#xff0c;为了进一步拓展医疗行业的发展空间&#xff0c;开始选择布局线上渠道&#xff0c;互联网医院平台的出现解决了线下就医的一些困境&#xff0c;比如改善人流如潮的情况&#xff0c;提升医护人员的工作效率&#xff0c;那么…

AutoSAR系列讲解(入门篇)3.1-RTE概述

一、什么是RTE RTE的作用有点像一个快递中转站或者说是电话接线员&#xff08;就是上个世界那种要先打电话到接线员那里&#xff0c;然后通过接线员转接电话线到目的地&#xff09;&#xff0c;其作 用就是将一个SWC的信息通过RTE连接到其他SWC或者BSW上。且RTE具有管理这些信…

前端新增校验关键属性是否重复

需求&#xff1a;前端新增某个属性时&#xff0c;该属性下可新增列表&#xff0c;列表编码禁止重复&#xff08;未提交该属性时前端校验列表编码是否重复&#xff09; js&#xff1a;新增后校验 let arrayCode if (this.collectionPointList.length 0) {this.collectionPoint…

自制聊天机器人实现与chatgpt或微信好友对话【附代码】

闲来无事&#xff0c;想实现一个可与chatgpt或者微信好友对话的聊天机器人。该聊天机器人还可应用于QQ好友或者其他地方的语音输入。功能还是比较简单的&#xff0c;后期会慢慢更新&#xff0c;让人机交互体验感不断提升。 项目描述&#xff1a; 语音输入"开启语音助手&…

Linux常用命令——fmt命令

在线Linux命令查询工具 fmt 读取文件后优化处理并输出 补充说明 fmt命令读取文件的内容&#xff0c;根据选项的设置对文件格式进行简单的优化处理&#xff0c;并将结果送到标准输出设备。 语法 fmt(选项)(参数)选项 -c或--crown-margin&#xff1a;每段前两列缩排&#…

Django期末复习总结【内含思维导图帮助梳理】

Django-最下面有笔记的下载链接 初始Django框架 MTV设计模式 Model&#xff08;模型&#xff09; Template&#xff08;模板&#xff09; View&#xff08;视图&#xff09; Django项目框架搭建 创建项目骨架 django-admin startproject my_project1 启动服务 python mana…

2 线程基础知识复习

1、并发相关Java包 涉及到的包内容 java.util.concurrent java.util.concurrent.atomic java.util.concurrent.locks 2、并发始祖 3、start线程解读 初始程序 public static void main(String[] args) {Thread t1 new Thread(() ->{},"t1");t1.start();}//…

从功能测试到自动化测试,待遇翻倍,我整理的超全学习指南!

在这个吃技术的IT行业来说&#xff0c;我刚入行的时候每天做的也是最基础的工作&#xff0c;但是随着时间的消磨&#xff0c;我产生了对自我和岗位价值和意义的困惑。一是感觉自己在浪费时间&#xff0c;另一个就是做了快2年的测试&#xff0c;感觉每天过得浑浑噩噩&#xff0c…

一个JVM参数,服务超时率降了四分之三

先说结论&#xff1a;通过优化Xms&#xff0c;改为和Xmx一致&#xff0c;使系统的超时率降了四分之三 1. 背景 一个同事说他负责的服务在一次上线之后超时率增加了一倍 2. 分析 2.1 机器的监控 首先找了一台机器&#xff0c;看了监控 上线后最明显的变化就是CPU使用率变高了…

Redis6之主从复制

主从复制 是指将一台Redis服务器的数据&#xff0c;复制到其他Redis服务器。前者称为主节点&#xff0c;后者称为从节点&#xff1b;数据复制是单向的&#xff0c;只能由主节点复制到从节点&#xff1b;主节点以写为主&#xff0c;从节点以读为主。 特点 1.使用异步复制&#…