spring扩展点之三:Spring 的监听事件 ApplicationListener 和 ApplicationEvent 用法,在spring启动后做些事情

服务网关zuul之七:zuul中的动态刷新路由配置

观察者模式与监听模式

JDK自带的观察者模式

JDK自带的监听器模式

ApplicationEvent事件机制源码分析

背景

在开发工作中,用到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、初始化处理

在一些业务场景中,当容器初始化完成之后,需要处理一些操作,比如一些数据的加载、初始化缓存、特定任务的注册等等。一般来说一个项目启动时需要加载或者执行一些特殊的任务来初始化系统,通常的做法就是用servlet去初始化,但是servlet在使用spring bean时不能直接注入,还需要在web.xml配置,比较麻烦(见http://www.cnblogs.com/duanxz/p/3772979.html)。这个时候我们就可以使用Spring提供的ApplicationListener来进行操作。
本文以在Spring boot下的使用为例来进行说明。首先,需要实现ApplicationListener接口并实现onApplicationEvent方法。把需要处理的操作放在onApplicationEvent中进行处理:
然后,实例化ApplicationStartListener这个类,在Spring boot中通过一个配置类来进行实例化:
随后,启动Spring boot服务,打印出一下内容:
从打印的结果可以看出,ApplicationStartListener的onApplicationEvent方法在容器启动时已经被成功调用了。而此时初始化的容器为root容器。

下面给出例子:
首先创建一个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

  1. public class ActionEvent extends ApplicationEvent{
  2. public ActionEvent(Object source) {
  3. super(source);
  4. System.out.println("This is ActionEvent");
  5. }
  6. }

2. 建立监听类 ActionListener1.java、ActionListener2.java

  1. public class ActionListener1 implements ApplicationListener {
  2. public void onApplicationEvent(ApplicationEvent event) {
  3. if(event instanceof ActionEvent){
  4. System.out.println("ActionListener1: "+event.toString());
  5. }
  6. }
  7. }
  1. public class ActionListener2 implements ApplicationListener {
  2. public void onApplicationEvent(ApplicationEvent event) {
  3. if(event instanceof ActionEvent){
  4. System.out.println("ActionListener2: "+event.toString());
  5. }
  6. }
  7. }

3. 在 bean.xml 中注册事件类和监听类

  1. <bean id="loginaction" class="com.ayali.action.LoginAction"/>
  2. <bean id="listener1" class="com.ayali.action.ActionListener1"/>
  3. <bean id="listener2" class="com.ayali.action.ActionListener2"/>

4. 编写 需要发布事件的 loginAction.java

  1. public class LoginAction implements ApplicationContextAware{
  2. private ApplicationContext applicationContext;
  3. public void setApplicationContext(ApplicationContext applicationContext)
  4. throws BeansException {
  5. this.applicationContext = applicationContext;
  6. }
  7. public void login(String username, String password){
  8. ActionEvent event = new ActionEvent(username);
  9. this.applicationContext.publishEvent(event);
  10. }
  11. }

5. 编写测试方法

  1. public void testActionListener(){
  2. ApplicationContext ctx = new FileSystemXmlApplicationContext("bean.xml");
  3. LoginAction loginAction = (LoginAction) ctx.getBean("loginaction");
  4. loginAction.login("jack", "123");
  5. }

输出结果为:

    1. This is ActionEvent
    2. ActionListener1:com.ayali.action.ActionEvent[source=jack]
    3. ActionListener2:com.ayali.action.ActionEvent[source=jack]

如何使用spring中的Log4jConfigListener--删除的更多相关文章

  1. spring中的Log4jConfigListener作用和webapp.root的设置

    转:http://blog.sina.com.cn/s/blog_7bbf356c01016wld.html 使用spring中的Log4jConfigListener有如如下好处:     1. 动 ...

  2. 【Java EE 学习 50】【Spring学习第二天】【使用注解的DI实现】【spring中的继承】【动态代理伪hibernate实现】

    一.使用注解的DI实现 1.@Resource 使用该注解能够实现引用型属性的DI实现,该注解能够根据属性名和属性类型自动给属性赋值.一般使用@Resource(name="student& ...

  3. Spring中@Transactional事务回滚实例及源码

    一.使用场景举例 在了解@Transactional怎么用之前我们必须要先知道@Transactional有什么用.下面举个栗子:比如一个部门里面有很多成员,这两者分别保存在部门表和成员表里面,在删除 ...

  4. Spring 中JCA CCI分析--转载

    转载地址:http://blog.csdn.net/a154832918/article/details/6790612 J2EE提供JCA(Java Connector Architecture)规 ...

  5. Spring 中jdbcTemplate 实现执行多条sql语句

    说一下Spring框架中使用jdbcTemplate实现多条sql语句的执行: 很多情况下我们需要处理一件事情的时候需要对多个表执行多个sql语句,比如淘宝下单时,我们确认付款时要对自己银行账户的表里 ...

  6. JAVA面试题:Spring中bean的生命周期

    Spring 中bean 的生命周期短暂吗? 在spring中,从BeanFactory或ApplicationContext取得的实例为Singleton,也就是预设为每一个Bean的别名只能维持一 ...

  7. spring中加入log4j

    spring中加入log4j <context-param> <param-name>log4jConfigLocation</param-name> <pa ...

  8. Spring中@Autowired注解与自动装配

    1 使用配置文件的方法来完成自动装配我们编写spring 框架的代码时候.一直遵循是这样一个规则:所有在spring中注入的bean 都建议定义成私有的域变量.并且要配套写上 get 和 set方法. ...

  9. (转)Spring中ThreadLocal的认识

    我们知道Spring通过各种DAO模板类降低了开发者使用各种数据持久技术的难度.这些模板类都是线程安全的,也就是说,多个DAO可以复用同一个模板实例而不会发生冲突.我们使用模板类访问底层数据,根据持久 ...

  10. Spring中的AOP开发

    1.代理模式 找个人将你原本想做的事情给做了. 三个部分组成: 抽象主题角色:真实主题和代理主题的共同接口. 真实主题角色:定义了代理角色所代表的真实对象. 代理主题角色:含有对真实主题角色的引用.代 ...

随机推荐

  1. python学习笔记6(字典)

    映射:键值对的关系,键(key)映射值(value) 字典是Python唯一的映射类型 >>> phonebook = {'} >>> phonebook {'} ...

  2. asp.net web api 开发时应当注意的事项

    Self referencing when returning chain of objects. This can be solved using a design pattern called t ...

  3. 1509 -- Glass Beads POJ

    题意:求一个字符串的最小表示的开始下标 就当模板题写了 把字符串重复一遍,再建后缀自动机,贪心的选最小字典序在上面走len步 因为走出来的一定是子串,长度又是len,所以一定是原来的字符串旋转得到的, ...

  4. [JavaScript] js 判断闰年

    /** * 判断闰年函数 * @param {number} year 要判断的年份 * @return {bool} 返回布尔值 * * 其实只要满足下面几个条件即可. * 1.普通年能被4整除且不 ...

  5. js数组反转

    var _li = test.getElementsByTagName("li"), arrayObj = [].slice.apply(_li),//_li用apply调用sli ...

  6. 【莫比乌斯反演】关于Mobius反演与lcm的一些关系与问题简化(BZOJ 2154 crash的数字表格&&BZOJ 2693 jzptab)

    BZOJ 2154 crash的数字表格 Description 今天的数学课上,Crash小朋友学习了最小公倍数(Least Common Multiple).对于两个正整数a和b,LCM(a, b ...

  7. hdu 1018

    数学题  用的这个方法比较烂 g++超时  c++ 406ms /******************************************************************* ...

  8. CodeForces 32C

    额  找找规律吧  要用long long 才过. #include <cstdio> #include <algorithm> using namespace std; in ...

  9. 数据聚合 & 分组:新一代系统监控的核心功能

    遥想 2015 年 8 月 17 日,Cloud Insight 还在梳理功能原型,畅想 Cloud Insight 存在的意义:为什么阿里云用户需要使用 Cloud Insight 来加强管理. 而 ...

  10. 重新学struct,边界对齐,声明……与Union的区别

    在内存中,编译器按照成员列表顺序分别为每个结构体变量成员分配内存,当存储过程中需要满足边界对齐的要求时,编译器会在成员之间留下额外的内存空间. 如果想确认结构体占多少存储空间,则使用关键字sizeof ...