javascript
SpringBoot 使用注解实现消息广播功能
背景
在開發工作中,會遇到一種場景,做完某一件事情以后,需要廣播一些消息或者通知,告訴其他的模塊進行一些事件處理,一般來說,可以一個一個發送請求去通知,但是有一種更好的方式,那就是事件監聽,事件監聽也是設計模式中發布-訂閱模式、觀察者模式的一種實現。
觀察者模式:簡單的來講就是你在做事情的時候身邊有人在盯著你,當你做的某一件事情是旁邊觀察的人感興趣的事情的時候,他會根據這個事情做一些其他的事,但是盯著你看的人必須要到你這里來登記,否則你無法通知到他(或者說他沒有資格來盯著你做事情)。
對于Spring容器的一些事件,可以監聽并且觸發相應的方法。通常的方法有 2 種,ApplicationListener 接口和@EventListener 注解。
簡介
要想順利的創建監聽器,并起作用,這個過程中需要這樣幾個角色:
事件(event)可以封裝和傳遞監聽器中要處理的參數,如對象或字符串,并作為監聽器中監聽的目標。
監聽器(listener)具體根據事件發生的業務處理模塊,這里可以接收處理事件中封裝的對象或字符串。
事件發布者(publisher)事件發生的觸發者。
ApplicationListener 接口
ApplicationListener接口的定義如下:
它是一個泛型接口,泛型的類型必須是ApplicationEvent 及其子類,只要實現了這個接口,那么當容器有相應的事件觸發時,就能觸發 onApplicationEvent 方法。ApplicationEvent 類的子類有很多,Spring 框架自帶的如下幾個。
? ?簡單使用
使用方法很簡單,就是實現一個 ApplicationListener 接口,并且將加入到容器中就行。
@Component public?class?DefinitionApplicationListener?implements?ApplicationListener<ApplicationEvent>?{//?-----------?簡單使用-實現ApplicationListener?接口@Overridepublic?void?onApplicationEvent(ApplicationEvent?event)?{System.out.println("事件觸發:"?+?event.getClass().getName());} }啟動項目
@SpringBootApplication public?class?EngineSupportApplication?{public?static?void?main(String[]?args)?{SpringApplication.run(EngineSupportApplication.class);} }查看日志
2021-11-12?21:04:16.114??INFO?83691?---?[???????????main]?o.s.s.concurrent.ThreadPoolTaskExecutor??:?Initializing?ExecutorService?'applicationTaskExecutor' 事件觸發:org.springframework.context.event.ContextRefreshedEvent 2021-11-12?21:04:16.263??INFO?83691?---?[???????????main]?o.s.b.w.embedded.tomcat.TomcatWebServer??:?Tomcat?started?on?port(s):?8080?(http)?with?context?path?'' 事件觸發:org.springframework.boot.web.servlet.context.ServletWebServerInitializedEvent 2021-11-12?21:04:16.266??INFO?83691?---?[???????????main]?com.alibaba.EngineSupportApplication?????:?Started?EngineSupportApplication?in?1.975?seconds?(JVM?running?for?3.504) 事件觸發:org.springframework.boot.context.event.ApplicationStartedEvent 事件觸發:org.springframework.boot.context.event.ApplicationReadyEvent
自定義事件以及監聽
? 定義事件
public?class?DefinitionEvent?extends?ApplicationEvent?{public?boolean?enable;/***?Create?a?new?ApplicationEvent.**?@param?source?the?object?on?which?the?event?initially?occurred?(never?{@code?null})*?@param?enable*/public?DefinitionEvent(Object?source,?boolean?enable)?{super(source);this.enable?=?enable;}? 定義監聽器
@Component public?class?DefinitionApplicationListener?implements?ApplicationListener<ApplicationEvent>?{//?-----------?簡單使用-實現ApplicationListener?接口@Overridepublic?void?onApplicationEvent(ApplicationEvent?event)?{System.out.println("事件觸發:"?+?event.getClass().getName());}//?-----------?自定義事件以及監聽@Autowiredprivate?ApplicationEventPublisher?eventPublisher;/***?事件發布方法*/public?void?pushListener(String?msg)?{eventPublisher.publishEvent(new?DefinitionEvent(this,?false));} }@EventListener 注解
? 簡單使用
除了通過實現接口,還可以使用@EventListener 注解,實現對任意的方法都能監聽事件。
在任意方法上標注@EventListener 注解,指定 classes,即需要處理的事件類型,一般就是 ApplicationEvent及其子類,可以設置多項。
@Component public?class?DefinitionAnnotationEventListener?{@EventListener(classes?=?{DefinitionEvent.class})public?void?listen(DefinitionEvent?event)?{System.out.println("注解監聽器:"?+?event.getClass().getName());}}此時,就可以有一個發布,兩個監聽器監聽到發布的消息了,一個是注解方式,一個是非注解方式
結果:
注解監聽器:com.alibaba.spring.context.event.DefinitionEvent 事件觸發:com.alibaba.spring.context.event.DefinitionEvent原理
其實上面添加@EventListener注解的方法被包裝成了ApplicationListener對象,上面的類似于下面這種寫法,這個應該比較好理解。
@Component public?class?DefinitionAnnotationEventListener?implements?ApplicationListener<DefinitionEvent>?{@Overridepublic?void?onApplicationEvent(DefinitionEvent?event)?{System.out.println("注解監聽器:"?+?event.getMsg());} }查看SpringBoot的源碼,找到下面的代碼,因為我是Tomcat環境,這里創建的ApplicationContext是org.springframework.bootweb.servlet.context.AnnotationConfigServletWebServerApplicationContext
protected?ConfigurableApplicationContext?createApplicationContext()?{Class<?>?contextClass?=?this.applicationContextClass;if?(contextClass?==?null)?{try?{switch?(this.webApplicationType)?{case?SERVLET:contextClass?=?Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);break;case?REACTIVE:contextClass?=?Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);break;default:contextClass?=?Class.forName(DEFAULT_CONTEXT_CLASS);}}catch?(ClassNotFoundException?ex)?{throw?new?IllegalStateException("Unable?create?a?default?ApplicationContext,?"?+?"please?specify?an?ApplicationContextClass",ex);}}return?(ConfigurableApplicationContext)?BeanUtils.instantiateClass(contextClass);}構造方法如下
public?AnnotationConfigApplicationContext()?{this.reader?=?new?AnnotatedBeanDefinitionReader(this);this.scanner?=?new?ClassPathBeanDefinitionScanner(this); }進入AnnotatedBeanDefinitionReader里面
/***?Create?a?new?{@code?AnnotatedBeanDefinitionReader}?for?the?given?registry,*?using?the?given?{@link?Environment}.*?@param?registry?the?{@code?BeanFactory}?to?load?bean?definitions?into,*?in?the?form?of?a?{@code?BeanDefinitionRegistry}*?@param?environment?the?{@code?Environment}?to?use?when?evaluating?bean?definition*?profiles.*?@since?3.1*/public?AnnotatedBeanDefinitionReader(BeanDefinitionRegistry?registry,?Environment?environment)?{Assert.notNull(registry,?"BeanDefinitionRegistry?must?not?be?null");Assert.notNull(environment,?"Environment?must?not?be?null");this.registry?=?registry;this.conditionEvaluator?=?new?ConditionEvaluator(registry,?environment,?null);AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);}再進到AnnotationConfigUtils的方法里面,省略了一部分代碼,可以看到他注冊了一個EventListenerMethodProcessor類到工廠了。這是一個BeanFactory的后置處理器。
public?static?Set<BeanDefinitionHolder>?registerAnnotationConfigProcessors(BeanDefinitionRegistry?registry,?@Nullable?Object?source)?{DefaultListableBeanFactory?beanFactory?=?unwrapDefaultListableBeanFactory(registry);.................????if?(!registry.containsBeanDefinition(EVENT_LISTENER_PROCESSOR_BEAN_NAME))?{RootBeanDefinition?def?=?new?RootBeanDefinition(EventListenerMethodProcessor.class);def.setSource(source);beanDefs.add(registerPostProcessor(registry,?def,?EVENT_LISTENER_PROCESSOR_BEAN_NAME));}............return?beanDefs;}查看這個BeanFactory的后置處理器EventListenerMethodProcessor,下面方法,他會遍歷所有bean,找到其中帶有@EventListener的方法,將它包裝成ApplicationListenerMethodAdapter,注冊到工廠里,這樣就成功注冊到Spring的監聽系統里了。
@Overridepublic?void?afterSingletonsInstantiated()?{ConfigurableListableBeanFactory?beanFactory?=?this.beanFactory;Assert.state(this.beanFactory?!=?null,?"No?ConfigurableListableBeanFactory?set");String[]?beanNames?=?beanFactory.getBeanNamesForType(Object.class);for?(String?beanName?:?beanNames)?{if?(!ScopedProxyUtils.isScopedTarget(beanName))?{Class<?>?type?=?null;try?{type?=?AutoProxyUtils.determineTargetClass(beanFactory,?beanName);}catch?(Throwable?ex)?{//?An?unresolvable?bean?type,?probably?from?a?lazy?bean?-?let's?ignore?it.if?(logger.isDebugEnabled())?{logger.debug("Could?not?resolve?target?class?for?bean?with?name?'"?+?beanName?+?"'",?ex);}}if?(type?!=?null)?{if?(ScopedObject.class.isAssignableFrom(type))?{try?{Class<?>?targetClass?=?AutoProxyUtils.determineTargetClass(beanFactory,?ScopedProxyUtils.getTargetBeanName(beanName));if?(targetClass?!=?null)?{type?=?targetClass;}}catch?(Throwable?ex)?{//?An?invalid?scoped?proxy?arrangement?-?let's?ignore?it.if?(logger.isDebugEnabled())?{logger.debug("Could?not?resolve?target?bean?for?scoped?proxy?'"?+?beanName?+?"'",?ex);}}}try?{processBean(beanName,?type);}catch?(Throwable?ex)?{throw?new?BeanInitializationException("Failed?to?process?@EventListener?"?+"annotation?on?bean?with?name?'"?+?beanName?+?"'",?ex);}}}}}private?void?processBean(final?String?beanName,?final?Class<?>?targetType)?{if?(!this.nonAnnotatedClasses.contains(targetType)?&&!targetType.getName().startsWith("java")?&&!isSpringContainerClass(targetType))?{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);}}if?(CollectionUtils.isEmpty(annotatedMethods))?{this.nonAnnotatedClasses.add(targetType);if?(logger.isTraceEnabled())?{logger.trace("No?@EventListener?annotations?found?on?bean?class:?"?+?targetType.getName());}}else?{//?Non-empty?set?of?methodsConfigurableApplicationContext?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;}}}if?(logger.isDebugEnabled())?{logger.debug(annotatedMethods.size()?+?"?@EventListener?methods?processed?on?bean?'"?+beanName?+?"':?"?+?annotatedMethods);}}}}由方法生成Listener的邏輯由EventListenerFactory完成的,這又分為兩種,一種是普通的@EventLintener另一種是@TransactionalEventListener,是由兩個工廠處理的。
總結
上面介紹了@EventListener的原理,其實上面方法里還有一個@TransactionalEventListener注解,其實原理是一模一樣的,只是這個監聽者可以選擇在事務完成后才會被執行,事務執行失敗就不會被執行。
這兩個注解的邏輯是一模一樣的,并且@TransactionalEventListener本身就被標記有@EventListener,只是最后生成監聽器時所用的工廠不一樣而已。
往期推薦MyBatis 中為什么不建議使用 where 1=1?
MyBatis原生批量插入的坑與解決方案!
聊聊sql優化的15個小技巧
總結
以上是生活随笔為你收集整理的SpringBoot 使用注解实现消息广播功能的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 20 图|Nacos 手摸手教程
- 下一篇: 用JavaScript将字符串中的单词大