深入浅出Spring/SpringBoot 事件监听机制

Posted Javaesandyou

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入浅出Spring/SpringBoot 事件监听机制相关的知识,希望对你有一定的参考价值。

监听器模型

说明

事件监听机制可以理解为是一种观察者模式,有数据发布者(事件源)和数据接受者(监听器);

在Java中,事件对象都是继承java.util.EventObject对象,事件监听器都是java.util.EventListener实例;

EventObject对象不提供默认构造器,需要外部传递source参数,即用于记录并跟踪事件的来源;

Spring事件

Spring事件对象为ApplicationEvent,继承EventObject,源码如下:

public abstract class ApplicationEvent extends EventObject 

	/**
	 * Create a new ApplicationEvent.
	 * @param source the object on which the event initially occurred (never @code null)
	 */
	public ApplicationEvent(Object source) 
		super(source);
		this.timestamp = System.currentTimeMillis();
	

Spring事件监听器为ApplicationListener,继承EventListener, 源码如下:

public interface ApplicationListener<E extends ApplicationEvent> extends EventListener 
    void onApplicationEvent(E var1);

实现Spring事件监听有两种方式

  1. 面向接口编程,实现ApplicationListener接口;
  2. 基于注解驱动,@EventListener(Spring自定义的注解);

实例:

  1. 面向接口编程,实现ApplicationListener接口:

自定义事件对象:

public class MyApplicationEvent extends ApplicationEvent 
    public MyApplicationEvent(Object source) 
        super(source);
    

自定义事件监听器:

public class MyApplicationListener implements ApplicationListener<MyApplicationEvent> 
    @Override
    public void onApplicationEvent(MyApplicationEvent event) 
        System.out.println("收到事件:" + event);
    

启动服务并发布事件:

public class ApplicationEventBootstrap 

    public static void main(String[] args) 
        AnnotationConfigApplicationContext context =
                new AnnotationConfigApplicationContext();
        // 注册自定义事件监听器
        context.addApplicationListener(new MyApplicationListener());
        // 启动上下文
        context.refresh();
        // 发布事件,事件源为Context
        context.publishEvent(new MyApplicationEvent(context));
        // 结束
        context.close();
    

运行结果:

收到事件:com.xx.MyApplicationEvent[source=org.springframework.context.annotation.AnnotationConfigApplicationContext@cb0ed20, started on Sat May 16 16:32:04 CST 2020]
  1. 使用注解 @EventListener实现Spring事件监听:
@Component
public class MyApplicationListener2 

    @EventListener(MyApplicationEvent.class)
    public void onEvent(MyApplicationEvent event) 
        System.out.println("收到事件:" + event);
    

启动并发布事件:

public class ApplicationEventBootstrap 

    public static void main(String[] args) 
        AnnotationConfigApplicationContext context =
                new AnnotationConfigApplicationContext();
        // 注册自定义事件监听器
        context.register(MyApplicationListener2.class);
        // 启动上下文
        context.refresh();
        // 发布事件,事件源为Context
        context.publishEvent(new MyApplicationEvent(context));
        // 结束
        context.close();
    

运行结果:

收到事件:com.xx.MyApplicationEvent[source=org.springframework.context.annotation.AnnotationConfigApplicationContext@cb0ed20, started on Sat May 16 16:32:04 CST 2020]

通过实例可以看出,上面两种方式都可正常发布和接收事件。

实现原理

通过上面实例可以看出,context 可以发布事件,那底层是怎么发布的,让我们继续看源码:

public abstract class AbstractApplicationContext extends DefaultResourceLoader
		implements ConfigurableApplicationContext 
      protected void publishEvent(Object event, @Nullable ResolvableType eventType) 
        ...
        getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
        ...
      

通过源码我们可以看出,事件应该是通过
ApplicationEventMulticaster发布的,我们继续看:

public class SimpleApplicationEventMulticaster extends AbstractApplicationEventMulticaster

Spring 中事件发布都是通过
SimpleApplicationEventMulticaster来实现的

public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) 
		ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
		for (final ApplicationListener<?> listener : getApplicationListeners(event, type)) 
			Executor executor = getTaskExecutor();
			if (executor != null) 
        // 异步
				executor.execute(() -> invokeListener(listener, event));
			
			else 
				invokeListener(listener, event);
			
		
	

可以看出,如果设置了Executor则异步发送,否则同步;而且可以看出通过 resolveDefaultEventType(event) 对发布的事件类型进行了校验,这就是为什么我们可以直接使用泛型来指定我们想接收的事件对象, 比如上面的 ApplicationListener<MyApplicationEvent>。

private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) 
		try 
			listener.onApplicationEvent(event);
		

最后就使用对应的ApplicationListener进行接收和处理就行了,那么ApplicationListener是什么时候注册的呢?

如何添加ApplicationListener?

  1. 直接添加,使用content.addApplicationListener(上面实例中有使用);
  2. 将自定义的ApplicationListener注册为一个Bean,Spring再初始化Bean之后会添加,具体代码在ApplicationListenerDetector#postProcessAfterInitialization,判断一个Bean如果是ApplicationListener,则也是使用context.addApplicationListener添加;
  3. 使用注解@EventListener,在初始化Bean之后,会在EventListenerMethodProcessor中进行处理和添加;

第三种实现的源码如下(
EventListenerMethodProcessor中):

private void processBean(final String beanName, final Class<?> targetType) 
  ....
  // 获取public 且有@EventListener的方法 
  AnnotatedElementUtils.findMergedAnnotation(method, EventListener.class));
  ... 
  ApplicationListener<?> applicationListener = factory.createApplicationListener(beanName, targetType, methodToUse);                 
  // 添加监听器
  context.addApplicationListener(applicationListener); 

Spring内建事件

  • ContextRefreshedEvent: Spring应用上下文就绪事件;
  • ContextStartedEvent: Spring应用上下文启动事件;
  • ContextStopedEvent: Spring应用上下文停止事件;
  • ContextClosedEvent: Spring应用上下文关闭事件;

Spring Boot事件

Spring Boot事件是在Spring事件基础上进行的封装

public abstract class SpringApplicationEvent extends ApplicationEvent

事件对象改为SpringApplicationEvent,事件源为SpringApplication(Spring事件源为Context);

底层发布事件还是使用
SimpleApplicationEventMulticaster 对象,不过有点需要说明的是,Spring Boot 1.4开始,SpringApplication和ApplicationContext使用的都是SimpleApplicationEventMulticaster实例,但是两者属于不同的对象(1.0 ~ 1.3版本是同一个对象);

事件回顾:

public class EventBootstrap 

    public static void main(String[] args) 
        new SpringApplicationBuilder(Object.class)
                .listeners(event -> 
                    System.out.println("事件对象:"
                    + event.getClass().getSimpleName()
                    + " ,事件源:" + event.getSource().getClass().getSimpleName());
                )
                .web(WebApplicationType.NONE)
                .run(args)
                .close();
    

运行结果:

事件对象:ApplicationContextInitializedEvent ,事件源:SpringApplication
事件对象:ApplicationPreparedEvent ,事件源:SpringApplication
事件对象:ContextRefreshedEvent ,事件源:AnnotationConfigApplicationContext
事件对象:ApplicationStartedEvent ,事件源:SpringApplication
事件对象:ApplicationReadyEvent ,事件源:SpringApplication
事件对象:ContextClosedEvent ,事件源:AnnotationConfigApplicationContext

从结果可以看出,事件对象类型和事件源,以及事件发布顺序。

 

spring-boot-devtools (springboot的热部署)

热部署是什么

大家都知道在项目开发过程中,常常会改动页面数据或者修改数据结构,为了显示改动效果,往往需要重启应用查看改变效果,其实就是重新编译生成了新的 Class 文件,这个文件里记录着和代码等对应的各种信息,然后 Class 文件将被虚拟机的 ClassLoader 加载。

而热部署正是利用了这个特点,它监听到如果有 Class 文件改动了,就会创建一个新的 ClaassLoader 进行加载该文件,经过一系列的过程,最终将结果呈现在我们眼前。

类加载机制

Java 中的类经过编译器可以把代码编译为存储字节码的 Class 文件,该 Class 文件存储了各种信息,最终要加载到虚拟机中运行使用。

类加载机制(摘自《深入理解 Java 虚拟机》)

      虚拟机把描述类的数据从 Class 文件加载到内存中,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。

Spring Boot 实现热部署

Spring Boot 实现热部署很简单,我们使用上一章创建的项目 Springboot 创建简单的 web 交互应用 来演示。

第一种方式 略

着重介绍在IDEA中实现spring boot 的热部署:

1.导入依赖包(最好在建项目时 勾选上core-devTools)

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-devtools</artifactId>
     <scope>runtime</scope>
  <!--  
<optional>true</optional> 依赖包不传递,假如该包在父pom中,如果其他子项目需要使用热部署的依赖包,还必须需要导入
--> <optional>true</optional> </dependency>

2.加入编译节点(在springboot中已经有了spring-boot-maven-plugin,所以直接加入configuration标签内的内容即可)

 <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
          <!-- 如果没有该配置,这个devtools不会起作用,即应用不会restart--> <fork>true</fork> </configuration> </plugin> </plugins> </build>

3. 修改配置,实现idea自动编译

    3.1   Settings-complier- build project automatically --> 勾选

技术分享图片

  3.2、CTRL + SHIFT + A --> 在出现的对话框中输入 : registry --> 找到并勾选compiler.automake.allow.when.app.running 技术分享图片

技术分享图片

4.  编写测试方法

  4.1   修改返回值

  4.2   增加方法,测试是否可以正确访问新的方法

  4.3   增加新的测试类,测试是否可以正确访问

 

5.  说明:  

  <optional>true</optional>   表示该依赖是否传递(true 表示可以传递)

  如  该依赖在父pom中,并且有该<optional>true</optional>设置,其子module会实现自动部署。----这个与该配置无关,改成false还是会自动部署,因为这个是父子程序的原因。

 




以上是关于深入浅出Spring/SpringBoot 事件监听机制的主要内容,如果未能解决你的问题,请参考以下文章

spring-boot-devtools (springboot的热部署)

jQuery是怎样监听到事件的?

oninput事件(解决onkeyup无法监听到复制黏贴)

js 怎么监听到页面关闭或页面跳转事件

qt QListWidget 添加鼠标移动事件(mouseMoveEvent),让父窗体可以监听到鼠标移动

js 怎么监听到页面关闭或页面跳转事件