在实际开发过程中,常常遇到这种场景:
做完某一件事情以后,需要广播一些消息或者通知,告诉其他的模块进行一些事件处理,一般来说,可以一个一个发送请求去通知,但是有一种更好的方式,那就是事件监听,事件监听也是设计模式中 发布-订阅模式、观察者模式的一种实现。
观察者模式:在对象之间定义了一对多的依赖,这样一来,当一个对象改变状态,依赖它的对象会收到通知并自动更新。
Spring的事件为Bean和Bean之间的消息传递提供支持。当一个对象处理完某种任务后,通知另外的对象进行某些处理,常用的场景有进行某些操作后发送通知,消息、邮件等情况。
Spring提供5种标准的事件监听:
- 上下文更新事件(ContextRefreshedEvent):该事件会在ApplicationContext被初始化或者更新时发布。也可以在调用ConfigurableApplicationContext接口中的refresh()方法时被触发。
- 上下文开始事件(ContextStartedEvent):当容器ConfigurableApplicationContext的Start()方法开始/重新开始容器时触发该事件。
- 上下文停止事件(ContextStoppedEvent):当容ConfigurableApplicationContext的Stop()方法停止容器时触发该事件。
- 上下文关闭事件(ContextClosedEvent):当ApplicationContext被关闭时触发该事件。容器被关闭时,其管理的所有单例Bean都被销毁。
- 请求处理事件(RequestHandledEvent):在Web应用中,当一个http请求(request)结束触发该事件。
不过也有些实际场景并用不上框架提供的标准事件,这个时候我们就需要自定义事件监听
Spring的事件遵循的流程:
- 自定义事件,继承ApplicationEvent(org.springframework.context.ApplicationEvent)
- 定义监听事件,实现ApplicationListener(org.springframework.context.ApplicationListener)
- 使用容器触发事件
Spring中书写事件监听有两种方式,编程式和注解式,这里两种都进行简单的说明。因为现在正好在看boot方面的东西,这里就以boot项目为基础开启事件监听,boot也是spring全家桶的一部分,所以配置和使用和传统spring项目也没什么区别,当然,boot提倡无xml,所以这里开启监听的方式也用注解的方式进行。
一:自定义事件
package com.wzh.demo.event;
import org.springframework.context.ApplicationEvent;
/**
* 自定义监听方法测试类
*/
public class MyEvent extends ApplicationEvent{
private String msg;
/**
* 在自定义事件的构造方法中除了第一个source参数,其他参数都可以去自定义,
* 可以根据项目实际情况进行监听传参,这里就只定义个简单的String字符串的透传
* @param source
* @param msg
*/
public MyEvent(Object source,String msg) {
super(source);
this.msg = msg;
}
/**
* 自定义监听器触发的透传打印方法
* @param msg
*/
public void printMsg(String msg)
{
System.out.println("编程事件监听:" + msg);
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
二:事件监听
package com.wzh.demo.event.listener;
import com.wzh.demo.event.MyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
/**
* 测试用自定义监听器,监听事件为MyEvent
*/
@Component
public class MyLisenter implements ApplicationListener<MyEvent>{
/**
* 对监听到的事件进行处理
* @param myEvent
*/
@Override
public void onApplicationEvent(MyEvent myEvent) {
/*
这里不做处理,只对消息进行透传打印,实际情况,
可以根据项目进行逻辑进行处理
*/
myEvent.printMsg(myEvent.getMsg());
}
}
三:事件发布
package com.wzh.demo.service.impl;
import com.wzh.demo.event.MyEvent;
import com.wzh.demo.service.ListenerTestService;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service("listenerService")
public class ListenerServieImpl implements ListenerTestService{
/**
* 上下文对象
*/
@Resource
private ApplicationContext applicationContext;
@Override
public void publish(String msg) {
//通过上下文对象发布监听
applicationContext.publishEvent(new MyEvent(this,msg));
System.out.println(msg);
}
}
junit测试类
import base.BaseJunit;
import com.wzh.demo.service.ListenerTestService;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
/**
* 事件监听测试
*/
public class listenerTest extends BaseJunit {
@Autowired
@Qualifier(value = "listenerService")
private ListenerTestService listenerTestService;
@Test
public void testEvent()
{
listenerTestService.publish("测试监听");
}
}
我们看到,这里已经成功的监听了,当调用listenerTestService.publish方法时会触发监听,进行一些列业务操作,如果listenerTestService类中还有其他方法,不过没有调用发布监听,是不会触发监听器的。
监听器的第二种自定义方式,注解方式,第一步自定义事件的步骤是一样的,只是在事件监听的步骤不一样,发布的步骤也是一样的。
事件监听
package com.wzh.demo.event.listener;
import com.wzh.demo.event.MyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
/**
* 测试用注解监听器
*/
@Component
public class MyAnnotationListener {
@EventListener
public void listener1(MyEvent event)
{
System.out.println("注解事件监听-2:"+ event.getMsg());
}
@EventListener
public void listener2(MyEvent event)
{
System.out.println("注解事件监听-1:"+ event.getMsg());
}
}
运行junit测试类
从运行结果我们可以看到是先执行的注解事件监听,然后执行的编程事件监听。这个时候代码的执行顺序是线程同步的,在执行listenerTestService.publish的过程中,根据代码顺序,代码第一步是触发监听,这个时候就会执行监听,监听的方法执行完成后才会执行后面的代码。不过大多数情况,我们是希望代码异步执行,监听器和业务逻辑为两个线程执行,实现的方法很多,列如自己写个多线程,开个线程池什么的。这里也提供一个spring支持的@Async注解方式开启异步方法。这里因为用的boot,所以在boot的启动类中进行配置。boot讲究开箱即用,并不用专门去配置监听器什么的。这里只展示启动类,其他的什么application.properties,pom.xml这些配置boot项目的东西就不在这里做说明。
package com.wzh.application;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.apache.ibatis.io.VFS;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.support.SpringBootServletInitializer;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
/**
* Spring Boot 应用启动类,这里继承SpringBootServletInitializer并重写SpringApplicationBuilder方法
* 是为了打包为war进行外部tomcat的部署
*/
@SpringBootApplication // Spring Boot 应用的标识
@EnableTransactionManagement // 启动注解事务 等同于传统Spring 项目中xml配置<tx:annotation-driven />
@EnableAsync //异步调用
@ComponentScan(basePackages = { "com.wzh"}) // 指定spring管理路径,就是那些bean 注解的路径
@MapperScan({ "com.wzh.**.mapper" }) // mapper 接口类扫描包配置,两个*为目录通配符
public class Application extends SpringBootServletInitializer{
// 程序启动入口
public static void main(String[] args) {
// 启动嵌入式的 Tomcat 并初始化 Spring 环境及其各 Spring 组件
SpringApplication.run(Application.class, args);
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
// 创建数据源,因为用是mybatis-spring 1.2 取消了数据源的自动注入,所以这里需要手动配置
@Bean
@ConfigurationProperties(prefix = "spring.datasource")// 指定数据源的前缀 ,在application.properties文件中指定
public DataSource dataSource() {
return new DataSource();
}
// 创建SqlSessionFactory
@Bean
public SqlSessionFactory sqlSessionFactoryBean() throws Exception {
//解决myBatis下 不能嵌套jar文件的问题
VFS.addImplClass(SpringBootVFS.class);
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource());
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
// 两个*为目录通配符
sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath:/mapper/**/*.xml"));
//指定扫描别名包的路径,多个bean的扫描路径,拼接以分号隔开
String typeAliasesPackage = "com.wzh.demo.domain;";
sqlSessionFactoryBean.setTypeAliasesPackage(typeAliasesPackage);
return sqlSessionFactoryBean.getObject();
}
// 创建事物管理器
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
//自定义线程池,当配置多个executor时,被@Async("id")指定使用;也被作为线程名的前缀
@Bean
public AsyncTaskExecutor taskExecutor()
{
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//线程池名字
executor.setThreadNamePrefix("asyncExecutor");
//最大线程数
executor.setMaxPoolSize(50);
//最小线程数
executor.setCorePoolSize(3);
// 使用预定义的异常处理类
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 自定义拒绝策略
/*executor.setRejectedExecutionHandler(new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
//........
}
});*/
return executor;
}
}
一些参数的用法,这里是xml中配置的参数的用法,我们用的是代码的操作,调用对应方法的意义也是一样的。
id:当配置多个executor时,被@Async(“id”)指定使用;也被作为线程名的前缀。
pool-size:
core size:最小的线程数,缺省:1
max size:最大的线程数,缺省:Integer.MAX_VALUE
queue-capacity:当最小的线程数已经被占用满后,新的任务会被放进queue里面,当这个queue的capacity也被占满之后,pool里面会创建新线程处理这个任务,直到总线程数达到了max size,这时系统会拒绝这个任务并抛出TaskRejectedException异常(缺省配置的情况下,可以通过rejection-policy来决定如何处理这种情况)。缺省值为:Integer.MAX_VALUE
keep-alive:超过core size的那些线程,任务完成后,再经过这个时长(秒)会被结束掉
rejection-policy:当pool已经达到max size的时候,如何处理新任务
ABORT(缺省):抛出TaskRejectedException异常,然后不执行
DISCARD:不执行,也不抛出异常
DISCARD_OLDEST:丢弃queue中最旧的那个任务
CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行
稍微修改监听,在监听器三做个@Async,并做个sleep,方便测试。
package com.wzh.demo.event.listener;
import com.wzh.demo.event.MyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
/**
* 测试用注解监听器
*/
@Component
public class MyAnnotationListener {
@EventListener
public void listener1(MyEvent event)
{
System.out.println("注解事件监听-2:"+ event.getMsg());
}
@EventListener
@Async
public void listener2(MyEvent event)
{
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("注解事件监听-1:"+ event.getMsg());
}
}
运行junit测试类
这里有看到报错java.lang.InterruptedException: sleep interrupted
做个错误是因为代码在使用单元测试因为单元测试启动的主线程很快就结束了,而子线程确sleep3秒,使得主线程强行打断子线程的sleep,因此抛出异常,解决办法是可以在单元测试的最后加上sleep(4*1000),目的是不让主线程在子线程前结束。这里我们暂时忽略做个错误,单纯看控制台输出,我们看到,我们先咨询了注解监听2,然后执行到注解监听1方法的时候休眠3秒,因为注解监听1是个异步方法,会开启一个子线程,做个时候子线程休眠3秒,主线程继续执行编程事务监听,然后执行原本方法中的打印操作,子线程休眠时间到后执行的子线程注解监听1的打印。
到这里就完成了解了自定义事件监听的使用方式了,等有空了再研究标准的事件监听,那个多用于项目启动的时候一些常年或者权限的加载。