spring扩展点之三:Spring 的监听事件 ApplicationListener 和 ApplicationEvent 用法,在spring启动后做些事情
《spring扩展点之三:Spring 的监听事件 ApplicationListener 和 ApplicationEvent 用法,在spring启动后做些事情》
背景
在开发工作中,用到spring cloud的zuul,zuul中的动态刷新zuul的路由信息中用到了事件监听,事件监听也是设计模式中 发布-订阅模式、观察者模式的一种实现。
在spring-cloud-netflix-core-1.4.4.RELEASE.jar中org.springframework.cloud.netflix.zuul.RoutesRefreshedEvent.java
package org.springframework.cloud.netflix.zuul; import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.context.ApplicationEvent; /**
* @author Dave Syer
*/
@SuppressWarnings("serial")
public class RoutesRefreshedEvent extends ApplicationEvent { private RouteLocator locator; public RoutesRefreshedEvent(RouteLocator locator) {
super(locator);
this.locator = locator;
} public RouteLocator getLocator() {
return this.locator;
} }
观察者模式:简单的来讲就是你在做事情的时候身边有人在盯着你,当你做的某一件事情是旁边观察的人感兴趣的事情的时候,他会根据这个事情做一些其他的事,但是盯着你看的人必须要到你这里来登记,否则你无法通知到他(或者说他没有资格来盯着你做事情)。
正文
要想顺利的创建监听器,并起作用,这个过程中需要这样几个角色:
1、事件(event)可以封装和传递监听器中要处理的参数,如对象或字符串,并作为监听器中监听的目标。
2、监听器(listener)具体根据事件发生的业务处理模块,这里可以接收处理事件中封装的对象或字符串。
3、事件发布者(publisher)事件发生的触发者。
在Spring中的,如果一个Bean实现了ApplicationListener接口,并且已经发布到容器中去,每次ApplicationContext发布一个ApplicationEvent事件,这个Bean就会接到通知。Spring事件机制是观察者模式的实现。
Spring中提供的标准事件:
ContextRefreshEvent,当ApplicationContext容器初始化完成或者被刷新的时候,就会发布该事件。比如调用ConfigurableApplicationContext接口中的refresh()方法。此处的容器初始化指的是所有的Bean都被成功装载,后处理(post-processor)Bean被检测到并且激活,所有单例Bean都被预实例化,ApplicationContext容器已经可以使用。只要上下文没有被关闭,刷新可以被多次触发。XMLWebApplicationContext支持热刷新,GenericApplicationContext不支持热刷新。
ContextStartedEvent,当ApplicationContext启动的时候发布事件,即调用ConfigurableApplicationContext接口的start方法的时候。这里的启动是指,所有的被容器管理生命周期的Bean接受到一个明确的启动信号。在经常需要停止后重新启动的场合比较适用。
ContextStoppedEvent,当ApplicationContext容器停止的时候发布事件,即调用ConfigurableApplicationContext的close方法的时候。这里的停止是指,所有被容器管理生命周期的Bean接到一个明确的停止信号。
ContextClosedEvent,当ApplicationContext关闭的时候发布事件,即调用ConfigurableApplicationContext的close方法的时候,关闭指的是所有的单例Bean都被销毁。关闭上下后,不能重新刷新或者重新启动。
RequestHandledEvent,只能用于DispatcherServlet的web应用,Spring处理用户请求结束后,系统会触发该事件。
实现
ApplicationEvent,容器事件,必须被ApplicationContext发布。
ApplicationListener,监听器,可由容器中任何监听器Bean担任。
实现了ApplicationListener接口之后,需要实现方法onApplicationEvent(),在容器将所有的Bean都初始化完成之后,就会执行该方法。
观察者模式
观察者模式,Observer Pattern也叫作发布订阅模式Publish/Subscribe。定义对象间一对多的依赖关系,使得每当一个对象改变状态,则所有依赖与它的对象都会得到通知,并被自动更新。
观察者模式的几角色名称:
- Subject被观察者,定义被观察者必须实现的职责,它能动态的增加取消观察者,它一般是抽象类或者是实现类,仅仅完成作为被观察者必须实现的职责:管理观察者并通知观察者。
- Observer观察者,观察者接受到消息后,即进行更新操作,对接收到的信息进行处理。
- ConcreteSubject具体的被观察者,定义被观察者自己的业务逻辑,同时定义对哪些事件进行通知。
- ConcreteObserver具体的观察者,每个观察者接收到消息后的处理反应是不同的,每个观察者都有自己的处理逻辑。
观察者模式的优点
- 观察者和被观察者之间是抽象耦合,不管是增加观察者还是被观察者都非常容易扩展。
- 建立一套触发机制。
观察者模式的缺点
观察者模式需要考虑开发效率和运行效率问题,一个被观察者,多个观察者,开发和调试比较复杂,Java消息的通知默认是顺序执行的,一个观察者卡壳,会影响整体的执行效率。这种情况一般考虑异步的方式。
使用场景
- 关联行为场景,关联是可拆分的。
- 事件多级触发场景。
- 跨系统的消息交换场景,如消息队列的处理机制。
Java中的观察者模式
java.util.Observable类和java.util.Observer接口。
订阅发布模型
观察者模式也叫作发布/订阅模式。
一、非注解的监听器的实现方式
非注解的监听器的实现方式,这样有利于了解一下注解实现的原理
什么是ApplicationContext?
它是Spring的核心,Context我们通常解释为上下文环境,但是理解成容器会更好些。
ApplicationContext则是应用的容器。
Spring把Bean(object)放在容器中,需要用就通过get方法取出来。
ApplicationEven:是个抽象类,里面只有一个构造函数和一个长整型的timestamp。
ApplicationListener:是一个接口,里面只有一个onApplicationEvent方法。
package org.springframework.context;
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener { /**
* Handle an application event.
* @param event the event to respond to
*/
void onApplicationEvent(E event); }
所以自己的类在实现该接口的时候,要实装该方法。
如果在上下文中部署一个实现了ApplicationListener接口的bean,那么每当在一个ApplicationEvent发布到ApplicationContext时,这个bean得到通知。其实这就是标准的Oberver设计模式。
2.1、初始化处理
下面给出例子:
首先创建一个ApplicationEvent实现类:
import org.springframework.context.ApplicationEvent; public class EmailEvent extends ApplicationEvent {
/**
* <p>Description:</p>
*/
private static final long serialVersionUID = 1L;
public String address;
public String text; public EmailEvent(Object source) {
super(source);
} public EmailEvent(Object source, String address, String text) {
super(source);
this.address = address;
this.text = text;
} public void print(){
System.out.println("hello spring event!");
} }
给出监听器:
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener; public class EmailListener implements ApplicationListener { public void onApplicationEvent(ApplicationEvent event) {
if(event instanceof EmailEvent){
EmailEvent emailEvent = (EmailEvent)event;
emailEvent.print();
System.out.println("the source is:"+emailEvent.getSource());
System.out.println("the address is:"+emailEvent.address);
System.out.println("the email's context is:"+emailEvent.text);
} } }
<bean id="emailListener" class="com.spring.event.EmailListener"></bean>
测试类:
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("classpath:applicationContext.xml"); //HelloBean hello = (HelloBean) context.getBean("helloBean");
//hello.setApplicationContext(context);
EmailEvent event = new EmailEvent("hello","boylmx@163.com","this is a email text!");
context.publishEvent(event);
//System.out.println();
}
}
测试结果:
hello spring event!
the source is:hello
the address is:boylmx@163.com
the email's context is:this is a email text!
二、注解 实现事件监听
好处:不用每次都去实现ApplicationListener,可以在一个class中定义多个方法,用@EventListener来做方法级别的注解。例如:
package com.mu.listener; import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component; import com.mu.event.MyTestEvent; @Component
public class MyAnnotationListener { @EventListener
public void listener1(MyTestEvent event) {
System.out.println("注解监听器1:" + event.getMsg());
}
}
在实际工作中,事件监听经常会用在发送通知,消息、邮件等情况下,那么这个时候往往是需要异步执行的,不能在业务的主线程里面,那怎么样可以实现异步处理呢?当然你可以写一个线程,单独做这个事情,在此,我比较推荐的是用spring的@Async注解方式,一个简单的注解,就可以把某一个方法或者类下面的所有方法全部变成异步处理的方法,这样,就可以做到处理监听事件的时候也不会阻塞主进程了。
新增监听器listener2,在方法上加上@Async注解,但是此注解不能标注static修饰的方法
package com.mu.listener; import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component; import com.mu.event.MyTestEvent; @Component
public class MyAnnotationListener { @EventListener
public void listener1(MyTestEvent event) {
System.out.println("注解监听器1:" + event.getMsg());
} @EventListener
@Async
public void listener2(MyTestEvent event) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("注解监听器2:" + event.getMsg());
} }
想要启动注解方式的异步处理办法,还需要做一下配置
注解的应用范围:
类:表示这个类中的所有方法都是异步的
方法:表示这个方法是异步的,如果类也注解了,则以这个方法的注解为准
配置:executor:指定一个缺省的executor给@Async使用。
-------------------------------------------------------------------------------------------------------------------------
当spring 容器初始化完成后执行某个方法 防止onApplicationEvent方法被执行两次
在做web项目开发中,尤其是企业级应用开发的时候,往往会在工程启动的时候做许多的前置检查。
比如检查是否使用了我们组禁止使用的Mysql的group_concat函数,如果使用了项目就不能启动,并指出哪个文件的xml文件使用了这个函数。
而在Spring的web项目中,我们可以介入Spring的启动过程。我们希望在Spring容器将所有的Bean都初始化完成之后,做一些操作,这个时候我们就可以实现一个接口:
package com.yk.test.executor.processor
public class InstantiationTracingBeanPostProcessor implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
//需要执行的逻辑代码,当spring容器初始化完成后就会执行该方法。
}
}
同时在Spring的配置文件中,添加注入:
<bean class="com.yk.test.executor.processor.InstantiationTracingBeanPostProcessor"/>
但是这个时候,会存在一个问题,在web 项目中(spring mvc),系统会存在两个容器,一个是root application context ,另一个就是我们自己的 projectName-servlet context(作为root application context的子容器)。
这种情况下,就会造成onApplicationEvent方法被执行两次。为了避免上面提到的问题,我们可以只在root application context初始化完成后调用逻辑代码,其他的容器的初始化完成,则不做任何处理,修改后代码
如下:
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if(event.getApplicationContext().getParent() == null){//root application context 没有parent,他就是老大.
//需要执行的逻辑代码,当spring容器初始化完成后就会执行该方法。
}
}
Spring 的事件传播机制 是基于观察者模式(Observer)实现的,它可以将 Spring Bean 的改变定义为事件 ApplicationEvent,通过 ApplicationListener 监听 ApplicationEvent 事件,一旦Spring Bean 使用 ApplicationContext.publishEvent( ApplicationEvent event )发布事件后,Spring 容器会通知注册在 bean.xml 中所有 ApplicationListener 接口的实现类,最后 ApplicationListener 接口实现类判断是否响应刚发布出来的 ApplicationEvent 事件。
所以,要使用 Spring 事件传播机制需要以下四点:
1. 建立事件类,继承 ApplicationEvent 父类
2. 建立监听类,实现 ApplicationListener 接口
3. 在配置文件 bean.xml 中注册写好的所有 事件类 和 监听类
4. 需要发布事件的类 要实现 ApplicationContextAware 接口,并获取 ApplicationContext 参数
随后便可以开始使用 Spring 事件传播机制为我们服务:(为了讲解流程的连贯性,续以上步骤来测试)
4.1 在自己编写的需要发布事件的 Action 类中实例化 1 中编写好的事件类,并使用 ApplicationContext.publishEvent 发布事件
5. 通过 Spring 调用 Action 方法,观察输出结果(本文使用 Junit 测试)
以下为1-5步骤的源码:
1. 建立事件类 ActionEvent.java
- public class ActionEvent extends ApplicationEvent{
- public ActionEvent(Object source) {
- super(source);
- System.out.println("This is ActionEvent");
- }
- }
2. 建立监听类 ActionListener1.java、ActionListener2.java
- public class ActionListener1 implements ApplicationListener {
- public void onApplicationEvent(ApplicationEvent event) {
- if(event instanceof ActionEvent){
- System.out.println("ActionListener1: "+event.toString());
- }
- }
- }
- public class ActionListener2 implements ApplicationListener {
- public void onApplicationEvent(ApplicationEvent event) {
- if(event instanceof ActionEvent){
- System.out.println("ActionListener2: "+event.toString());
- }
- }
- }
3. 在 bean.xml 中注册事件类和监听类
- <bean id="loginaction" class="com.ayali.action.LoginAction"/>
- <bean id="listener1" class="com.ayali.action.ActionListener1"/>
- <bean id="listener2" class="com.ayali.action.ActionListener2"/>
4. 编写 需要发布事件的 loginAction.java
- public class LoginAction implements ApplicationContextAware{
- private ApplicationContext applicationContext;
- public void setApplicationContext(ApplicationContext applicationContext)
- throws BeansException {
- this.applicationContext = applicationContext;
- }
- public void login(String username, String password){
- ActionEvent event = new ActionEvent(username);
- this.applicationContext.publishEvent(event);
- }
- }
5. 编写测试方法
- public void testActionListener(){
- ApplicationContext ctx = new FileSystemXmlApplicationContext("bean.xml");
- LoginAction loginAction = (LoginAction) ctx.getBean("loginaction");
- loginAction.login("jack", "123");
- }
输出结果为:
- This is ActionEvent
- ActionListener1:com.ayali.action.ActionEvent[source=jack]
- ActionListener2:com.ayali.action.ActionEvent[source=jack]
spring扩展点之三:Spring 的监听事件 ApplicationListener 和 ApplicationEvent 用法,在spring启动后做些事情的更多相关文章
- Spring 的监听事件 ApplicationListener 和 ApplicationEvent 用法
什么是ApplicationContext? 它是spring的核心,Context我们通常解释为上下文环境,但是理解成容器会更好些. ApplicationContext则是应用的容器. Sprin ...
- 【转】NGUI研究院之三种方式监听NGUI的事件方法(七)
NGUI事件的种类很多,比如点击.双击.拖动.滑动等等,他们处理事件的原理几乎万全一样,本文只用按钮来举例. 1.直接监听事件 把下面脚本直接绑定在按钮上,当按钮点击时就可以监听到,这种方法不太好很不 ...
- (转)NGUI研究院之三种方式监听NGUI的事件方法
NGUI事件的种类很多,比如点击.双击.拖动.滑动等等,他们处理事件的原理几乎万全一样,本文只用按钮来举例. 1.直接监听事件 把下面脚本直接绑定在按钮上,当按钮点击时就可以监听到,这种方法不太好很不 ...
- springboot13 发布和监听事件
spring中的事件驱动模型Event(也叫发布订阅模式),是观察者模式的一个典型的应用 好处:业务解耦,在不影响原来业务逻辑的情况下,加入其它业务 场景: app上线后已实现用户注册功能,现需要在用 ...
- zookeeper 监听事件 PathChildrenCacheListener
zookeeper 监听事件 PathChildrenCacheListener PathChildrenCacheListener一次父节点注册,监听每次子节点操作,不监听自身和查询. 1.测试类: ...
- zookeeper 监听事件 NodeCacheListener
zookeeper 监听事件 NodeCacheListener NodeCacheListener一次注册,每次监听,但是监听不到操作类型,不知道是增加?删除?还是修改? 1.测试类: packag ...
- zookeeper 监听事件 CuratorWatcher
zookeeper 监听事件 CuratorWatcher CuratorWatcher一次注册只监听一次,不监听查询. 1.监听测试类 package com.qy.learn.zk.curator ...
- Redis集群环境下的键值空间监听事件实现方案
一直想记录工作中遇到的问题和解决的方法,奈何没有找到一方乐土,最近经常反思,是否需要记录平时的点滴,后台还是决定下定决心记录一些,以便以后用到的时候找不着,实现这样的一个功能主要也是业务所需要的. 需 ...
- Android中Button的五种监听事件
简单聊一下Android中Button的五种监听事件: 1.在布局文件中为button添加onClick属性,Activity实现其方法2.匿名内部类作为事件监听器类3.内部类作为监听器4.Activ ...
随机推荐
- 2018-2019-2 网络对抗技术 20165202 Exp1 PC平台逆向破解
一.基础知识 掌握NOP, JNE, JE, JMP, CMP汇编指令的机器码. NOP指令为空指令,当运行该指令时CPU不做任何事情,但是会占用一个指令的时间,当指令间需要有延时,可以插入NOP指令 ...
- 多种数据库之间的同步工具SymmetricDS
代码:https://github.com/JumpMind/symmetric-ds 原理: 通过触发器模式同步时,是将数据库的变化记录到某个系统表中,然后在客户端建立缓冲,并定期将变化push到接 ...
- C#winform菜单权限分配,与菜单同步的treeView树状菜单权限控制使用心得
在网上查了很多,发现没有讲述关于--C#winform菜单权限分配,与菜单同步的treeView树状菜单权限控制使用--的资料 自己研究了一个使用方法.下面来看看. 我有两个窗体:LOGINFRM,M ...
- 设计Popup Window
设计一个Popup window, 在其中实现分享到Facebook 和Twitter 功能. popup window 名称为 ShareView.xaml, 代码如下: <phone:Pho ...
- 特征工程 —— 特征重要性排序(Random Forest)
树模型天然会对特征进行重要性排序,以分裂数据集,构建分支: 1. 使用 Random Forest from sklearn.datasets import load_boston from skle ...
- python可视化爬虫实现“京东试用”批量申请
介绍: 环境:chromedriver 2.41.578700+ selenuim3.14.0 过程: 1.打开京东主页 2.登录京东 3.打开京东试用页面 4.获取商品列表 5.自动申请试用(该商品 ...
- 【error】segmentation fault分析
前言 调试代码的时候,可能会出现segmentation fault的bug,很难找到原因,在此总结一下可能的原因. SIGSEGV 原因分析 1.程序中的变量没有进行检查: 比如,没有对变量的大小进 ...
- [LeetCode&Python] Problem 897. Increasing Order Search Tree
Given a tree, rearrange the tree in in-order so that the leftmost node in the tree is now the root o ...
- Visual Studio 2017 以前的旧格式的 csproj Import 进来的 targets 文件有时不能正确计算属性(PropertyGroup)和集合(ItemGroup)
我在之前的博客中有教大家如何编写 NuGet 工具包,其中就有编写 .targets 文件. 我在实际的使用中,发现 Visual Studio 2017 带来的含 Sdk 的新 csproj 格式基 ...
- 龙儿经理嘴上经常说的B树
国内的数据结构教材一般是按照Knuth定义,即“阶”定义为一个节点的子节点数目的最大值. 对于一棵m阶B-tree,每个结点至多可以拥有m个子结点.各结点的关键字和可以拥有的子结点数都有限制 规定m阶 ...