Spring事件发布监听机制实现业务解耦

Spring事件发布监听机制实现业务解耦

[TOC]

参考链接:
Spring事件监听机制 - 知乎
深入浅出Spring/SpringBoot 事件监听机制 - 知乎
Spring事件发布监听
spring 事件及异步方法使用

1.引言

假设一个下单场景,订单创建成功后可能有一些后续逻辑要处理,但是和创建订单本身没有关系,此时就可以在创建订单完成后,发送一个消息,有相应部分的代码进行监听处理,避免代码耦合到一起

这样的解决思路类似于MQ,但是小项目有时候又不需要MQ这样的第三方队列来实现,那么就可以使用Spring Context包的事件发布监听的机制来进行处理

2.Spring事件发布监听机制

1.png

流程: 当事件源(发布者)发布事件时,相应监听此事件的监听者接收到事件对象并且进行处理

Spring的事件发布监听机制本质上就是发布-订阅,即生产者-消费者,也体现了设计模式中的观察者模式

3.三要素

  • ApplicationEvent:事件
  • ApplicationListener:事件监听者
  • ApplicationEventPublisher:事件发布者

3.1 事件(ApplicationEvent)

消息类:

@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class Message {

    private Long messageId;

    private String content;

}

事件包含的实体类

事件类:

@Getter
@Setter
public class MessageEvent extends ApplicationEvent {

    private static final long serialVersionUID = 4181929072911659524L;

    private Message message;

    public MessageEvent(Message message) {
        super(message);
        this.message = message;
    }

}

事件类,继承了ApplicationEvent,并且包含了传递实体类Message

MessageEvent的关系类图(Diagram):

2.png

其中ApplicationEvent的源码:

/**
 * Class to be extended by all application events. Abstract as it
 * doesn't make sense for generic events to be published directly.
 *
 * @author Rod Johnson
 * @author Juergen Hoeller
 * @see org.springframework.context.ApplicationListener
 * @see org.springframework.context.event.EventListener
 */
public abstract class ApplicationEvent extends EventObject {

    /** use serialVersionUID from Spring 1.2 for interoperability. */
    private static final long serialVersionUID = 7099057708183571937L;

    /** System time when the event happened. */
    private final long timestamp;


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


    /**
     * Return the system time in milliseconds when the event occurred.
     */
    public final long getTimestamp() {
        return this.timestamp;
    }

}

可以看出ApplicationEvent有记录发生event的时间,并且source的意义就是当做发布事件的实体类

3.2 事件监听者(ApplicationListener)

事件监听类:方式一(EventListener注解实现)

@Component
@Slf4j
public class MessageListener {


    @EventListener(value = MessageEvent.class)
    public void listen(MessageEvent event){
        log.info("\n██listener1线程:{}",Thread.currentThread().getThreadGroup()+ "/" +Thread.currentThread().getName());
        log.info("event:{}",event);
        // 处理逻辑
    }

}

注解实现监听的原理:

1:查看@EventListener注解的调用链,其中EventListenerMethodProcessor类的processBean方法(1)

// 截取的代码
Map<Method, EventListener> annotatedMethods = null;
try {
  annotatedMethods = MethodIntrospector.selectMethods(targetType,
      (MethodIntrospector.MetadataLookup<EventListener>) method ->
          AnnotatedElementUtils.findMergedAnnotation(method, EventListener.class));
}
catch (Throwable ex) {
  // An unresolvable type in a method signature, probably from a lazy bean - let's ignore it.
  if (logger.isDebugEnabled()) {
    logger.debug("Could not resolve methods for bean with name '" + beanName + "'", ex);
  }
}

获取所有被@EventListener注解修饰的Listener

2:EventListenerMethodProcessor类的processBean方法(2)

// 截取的代码
// 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;
        }
    }
}

使用Listener Factory类生产出所有被@EventListener的类注入进Spring Context

事件监听类:方式二(实现ApplicationListener接口)

@Component
@Slf4j
public class MessageListener2 implements ApplicationListener<MessageEvent> {

    @Override
    public void onApplicationEvent(MessageEvent event) {
        log.info("\n██listener2线程:{}",Thread.currentThread().getThreadGroup()+ "/" +Thread.currentThread().getName());
        log.info("event:{}",event);
        // 处理逻辑
    }
}

实现接口来实现监听的原理:

1:ApplicationListenerDetector的postProcessAfterInitialization方法

@Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        if (bean instanceof ApplicationListener) {
            // potentially not detected as a listener by getBeanNamesForType retrieval
            Boolean flag = this.singletonNames.get(beanName);
            if (Boolean.TRUE.equals(flag)) {
                // singleton bean (top-level or inner): register on the fly
                this.applicationContext.addApplicationListener((ApplicationListener<?>) bean);
            }
            else if (Boolean.FALSE.equals(flag)) {
                if (logger.isWarnEnabled() && !this.applicationContext.containsBean(beanName)) {
                    // inner bean with other scope - can't reliably process events
                    logger.warn("Inner bean '" + beanName + "' implements ApplicationListener interface " +
                            "but is not reachable for event multicasting by its containing ApplicationContext " +
                            "because it does not have singleton scope. Only top-level listener beans are allowed " +
                            "to be of non-singleton scope.");
                }
                this.singletonNames.remove(beanName);
            }
        }
        return bean;
    }

判断一个Bean如果是ApplicationListener,则也是使用context.addApplicationListener添加

总结: 相对于实现ApplicationListener接口来监听事件的方式,使用注解的方式更加简便,并且方式二一个监听类只能监听一个事件,方式一则可新增方法来监听多个其他的事件

3.3 事件发布者(ApplicationEventPublisher)

@Autowired
private ApplicationContext applicationContext;

@PostMapping("/sendMessage")
public String sendMessage(){
    log.info("\n██Test线程:{}",Thread.currentThread().getThreadGroup()+ "/" +Thread.currentThread().getName());
    Message newMessage = Message.builder()
            .messageId(20200610111500000L)
            .content("消息内容").build();
    MessageEvent event = new MessageEvent(newMessage);
    // 事件发布
    applicationContext.publishEvent(event);
    return "消息发送成功";
}

事件发布原理分析:

1:查看ApplicationContext类

public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,
        MessageSource, ApplicationEventPublisher, ResourcePatternResolver {

ApplicationContext实现了ApplicationEventPublisher类的pulish方法,ApplicationContext的抽象类AbstractApplicationContext里阐述了具体的publishEvent方法

2:AbstractApplicationContext类的publishEvent方法

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;
        if (event instanceof ApplicationEvent) {
            applicationEvent = (ApplicationEvent) event;
        }
        else {
            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);
        }

        // Publish event via parent context as well...
        if (this.parent != null) {
            if (this.parent instanceof AbstractApplicationContext) {
                ((AbstractApplicationContext) this.parent).publishEvent(event, eventType);
            }
            else {
                this.parent.publishEvent(event);
            }
        }
    }

getApplicationEventMulticaster().multicastEvent方法可以看出事件是通过SimpleApplicationEventMulticaster的multicastEvent方法发布的

3:SimpleApplicationEventMulticaster的multicastEvent方法

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

可以看出如果设置了Executor(线程池)的话,则异步执行监听方法,否则执行同步方法
所以后续可以用设置Executor的方法实现异步

4.测试

Postman调用sendMessage接口,成功实现功能

3.png

5.支持异步

5.1 设置Executor的方法实现异步(推荐)

@Slf4j
@Configuration
public class EventConfig {

    @Bean(name = "applicationEventMulticaster")
    public ApplicationEventMulticaster simpleApplicationEventMulticaster() {
        SimpleApplicationEventMulticaster eventMulticaster
                = new SimpleApplicationEventMulticaster();

        eventMulticaster.setTaskExecutor(new SimpleAsyncTaskExecutor());
        return eventMulticaster;
    }

}

5.2 异步注解

1:主类新增@EnableAsync注解开启异步

@SpringBootApplication
@EnableAsync
public class SpringEventDemoApplication {

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

2:监听类监听方法新增注解@Async

@Component
@Slf4j
public class MessageListener {


    @Async
    @EventListener(value = MessageEvent.class)
    public void listen(MessageEvent event){
        ThreadUtil.sleep(6000);
        log.info("\n██listener1线程:{}",Thread.currentThread().getThreadGroup()+ "/" +Thread.currentThread().getName());
        log.info("event:{}",event);
    }

}

这种做法可以实现异步,但是其实有点违背了Spring事件机制的设计初衷,所以推荐第一种做法实现异步

6.Demo代码

GitHub - Spring事件发布监听机制实现业务解耦

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342