首页 > 其他分享 >Spring带泛型的ApplicationEvent无法监听问题分析(转载)

Spring带泛型的ApplicationEvent无法监听问题分析(转载)

时间:2024-11-11 16:42:42浏览次数:4  
标签:Spring 带泛 ApplicationEvent event public 事件 eventType ResolvableType

1 背景

在开发过程中,经常遇到发送事件来通知其他模块进行相应的业务处理;笔者实用的是spring自带的ApplicationEventPublisherEventListener进行事件的发收;
但是开发时遇到一个问题:
如果事件很多,但是事件模式都差不多,就需要定义很多事件类来分别表示各种事件,例如,我们进行数据同步,每同步一条数据都要发送对应的事件,伪代码如下:

//事件类
class RegionEvent {
  private Region region;
  private OperationEnum operation;
}

class UserEvent {
  private User user;
  private OperationEnum operation;
}

//插入一个区域
regionDao.insert(Region region);
//发送插入区域事件
publisher.publishEvent(new RegionEvent(region, INSERT));

//更新一个用户
userDao.update(User user);
//发送更新用户事件
publisher.publishEvent(new UserEvent(user, UPDATE));

//区域事件监听器
@EventListener
public void onRegionEvent(RegionEvent event) {
    log.info("receive event: {}", event);
}

//用户事件监听器
@EventListener
public void onUserEvent(UserEvent event) {
    log.info("receive event: {}", event);
}

此时,我们发现有太多冗余的代码,因为每插入一种类型的数据,就要对应的建立一个和该类型相关的事件类;自然而然地,我们想到可以使用泛型来简化以上逻辑。

1 泛型事件遇到的问题

我们定义一种泛型事件,来重新实现以上的逻辑,此时我们发现一个问题:发送的事件根本监听不到,伪代码如下:

class BaseEvent<T> {
  private T data;
  private OperationEnum operation;
}

//发送插入区域事件
publisher.publishEvent(new BaseEvent<>(region, INSERT));
//发送更新用户事件
publisher.publishEvent(new BaseEvent<>(user, UPDATE));

//区域事件监听器
@EventListener
public void onRegionEvent(BaseEvent<Region> event) {
    log.info("receive event: {}", event);
}

//用户事件监听器
@EventListener
public void onUserEvent(BaseEvent<User> event) {
    log.info("receive event: {}", event);
}

这是由于spring在解析事件类型时,并没有对事件的泛型进行解析,导致在运行时所有publish的事件都被spring解析成了BaseEvent<?>事件,如果采用如下代码,则会监听到所有事件:

@EventListener
public void onUserEvent(BaseEvent<Object> event) {
    log.info("receive event: {}", event);
}

@EventListener
public void onUserEvent(BaseEvent event) {
    log.info("receive event: {}", event);
}

2 解决方法

查阅了spring的文档后,发现spring已经考虑到这一点,官方文档原文如下:

In certain circumstances, this may become quite tedious if all events follow the same structure. In such a case, you can implement ResolvableTypeProvider to guide the framework beyond what the runtime environment provides. The following event shows how to do so:
大概翻译一下:
在某些情况下,如果所有事件类型都遵循相同的结构,这会是特别恶心的一件事。在这种情况下,你可以通过实现ResolvableTypeProvider接口,在运行时基于环境提供的信息来引导框架

我们基于spring提供的方法,对原有的泛型事件进行改造:

public class BaseEvent<T> implements ResolvableTypeProvider {
  private T data;
  private OperationEnum operation;

  @Override
  public ResolvableType getResolvableType() {
      return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forClass(getData().getClass()));
  }
}

此时,使用上文的监听器就可以监听到对应的事件了;

3 原理

事件监听器和事件是通过事件类型进行匹配的,而事件类型的publish源码在AbstractApplicationContext类的
protected void publishEvent(Object event, @Nullable ResolvableType eventType)
方法中,如下:

        ApplicationEvent applicationEvent;
        
        if (event instanceof ApplicationEvent) {
            //对于继承ApplicationEvent的事件,
            applicationEvent = (ApplicationEvent) event;
        }
        else {
            //对于非继承ApplicationEvent的事件,包装成PayloadApplicationEvent,
            //然后通过getResolvableType()获取事件类型
            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 {
            getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
        }

然后进入multicastEvent(applicationEvent, eventType)方法:

    @Override
    public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
        //这里对于ApplicationEvent的子类事件,进行解析事件类型
        ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
        Executor executor = getTaskExecutor();
        //根据上面解析到的eventType,获取对应的监听器,并依次执行回调方法
        for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
            if (executor != null) {
                executor.execute(() -> invokeListener(listener, event));
            }
            else {
                invokeListener(listener, event);
            }
        }
    }

可以发现,关键在于如何解析事件类型,分别进入上文中resolveDefaultEventType()方法和getResolvableType()方法,可以看到解析事件类型的具体细节如下:

//针对PayloadApplicationEvent,通过下面的方法处理,可见
    @Override
    public ResolvableType getResolvableType() {
        return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(getPayload()));
    }
//对于继承了ApplicationEvent的事件类
    private ResolvableType resolveDefaultEventType(ApplicationEvent event) {
        return ResolvableType.forInstance(event);
    }

上述两个方法用于根据事件构造事件的ResolvableType,关键代码在ResolvableType.forInstance():

    public static ResolvableType forInstance(Object instance) {
        Assert.notNull(instance, "Instance must not be null");
        if (instance instanceof ResolvableTypeProvider) {
            ResolvableType type = ((ResolvableTypeProvider) instance).getResolvableType();
            if (type != null) {
                return type;
            }
        }
        return ResolvableType.forClass(instance.getClass());
    }

至此,可以看到,如果事件实现了ResolvableTypeProvider接口,则可以通过调用getResolvableType方法获取事件的带泛型类型,如果未实现该接口,则只能获取事件的原始类型,效果如下:

未实现接口的情况下:

实现接口后:

作者:TinyThing
链接:https://www.jianshu.com/p/fd0c358176b9
来源:简书

标签:Spring,带泛,ApplicationEvent,event,public,事件,eventType,ResolvableType
From: https://www.cnblogs.com/zhangqingyan/p/18540048

相关文章

  • springboot项目使用JpaRepository后启动报错A component required a bean named 'XXX'
    在最近的项目中我使用了JpaRepository作为数据处理的接口,但在调用接口时始终报错,经过查询后发现问题出在导入的包不对,我导入的包为org.springframework.dataspring-data-jpa2.6.9应该导入的包为org.springframework.bootspring-boot-starter-data-jpa2.6.9spring-dat......
  • Spring Security 防止 CSRF 攻击
    使用security是3.3.2版本1、启用CSRF,security自带功能1@Bean2publicSecurityFilterChainfilterChain(HttpSecurityhttpSecurity)throwsException{3//禁用默认的登录和退出4httpSecurity.formLogin(AbstractHttpConfigurer::di......
  • Spring学习笔记_30——事务接口PlatformTransactionManager
    PlatformTransactionManager是Spring框架中事务管理的核心接口,它负责管理事务的创建、提交和回滚等操作。源码/**Copyright2002-2020theoriginalauthororauthors.**LicensedundertheApacheLicense,Version2.0(the"License");*youmaynotusethis......
  • 《Spring Boot 应用开发技术文档分享》
    一、引言在当今的软件开发领域,快速、高效地构建企业级应用是开发者们追求的目标。SpringBoot作为一款强大的Java开发框架,以其简洁的配置、快速的开发速度和强大的功能,受到了广大开发者的青睐。本文将详细介绍SpringBoot的特点、优势以及在应用开发中的实际使用方法。......
  • 基于springboot+vue.js+uniapp小程序的企业资产管理系统附带文章源码部署视频讲解等
    文章目录前言详细视频演示具体实现截图核心技术介绍后端框架SpringBoot前端框架Vue持久层框架MyBaits为什么选择我代码参考数据库参考测试用例参考源码获取前言......
  • 基于springboot+vue.js+uniapp小程序的华强北商城二手手机管理系统附带文章源码部署视
    文章目录前言详细视频演示具体实现截图核心技术介绍后端框架SpringBoot前端框架Vue持久层框架MyBaits为什么选择我代码参考数据库参考测试用例参考源码获取前言......
  • 基于springboot+vue.js+uniapp小程序的汽车资讯网站附带文章源码部署视频讲解等
    文章目录前言详细视频演示具体实现截图核心技术介绍后端框架SpringBoot前端框架Vue持久层框架MyBaits为什么选择我代码参考数据库参考测试用例参考源码获取前言......
  • SpringBoot集成SpringSecurity并实现自定义认证
    目录一、SpringSecurity简介二、集成SpringSecurity1、引入依赖2、编写核心配置类3、数据库建表4、自定义session失效策略5、自定义认证6、重写loadUserByUsername方法7、登录页面和接口三、总结一、SpringSecurity简介SpringSecurity是一个能够为基于Spring的企......
  • 【9691】基于springboot+vue的地方美食分享网站
    作者主页:Java码库主营内容:SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app等设计与开发。收藏点赞不迷路 关注作者有好处文末获取免费源码项目描述困扰管理层的许多问题当中,地方美食分享管理一定是美食界不敢忽视的一块。......
  • springboot 校园设施报修管理系统-毕业设计源码33917
    摘 要随着互联网大趋势的到来,社会的方方面面,各行各业都在考虑利用互联网作为媒介将自己的信息更及时有效地推广出去,而其中最好的方式就是建立网络管理系统,并对其进行信息管理。由于现在网络的发达,校园设施报修管理通过网络进行信息管理掀起了热潮,所以针校园设施报修管理的......