《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]