案例分析引入事件监听
首先我们假定一个场景,我们此时有一个登录的功能,根据用户名和密码进行登录。然后此时想要添加一个新的功能,如果一个用户登录成功就给这个用户发送短信提醒该用户(正常情况是异地登录才会发送短信提醒,这里为了简化需求,只要登录就发送短信提醒用户)
我们先来看一种实现方式:
// 注意这里是伪代码
public void login(String username, String password) {
if(根据username查看数据库当中是否有这个用户) {
获取这个用户的数据库中的密码
if(使用传入进来的密码跟数据库中获取到的密码进行校验看是否正确){
登录成功
// ----------------------------
发送短信提醒用户登录成功
// ----------------------------
}else {
// 密码输入错误
}
}else{
// 没有这个用户,登录失败
}
}
那么此时这个实现方式有什么缺点吗?
我们来思考这样一个问题,如果此时需求发生变更,如果登录成功不发短信了,而是发送邮件,那么如果按照上面这种实现方式应该怎么办?
显然我们需要在login方法中进行修改,把发送短信的逻辑改为发送邮件的逻辑,但是请注意,如果我们的项目真实的跑在服务器上,我们是不能将项目停下来然后修改他的源码的。
显然这就是一个问题,那为什么会导致这个问题的发生呢?这是因为我们在写login方法的时候耦合度太高了,说到这个耦合度高,可能有的同学会想到aop,aop不就是解耦的吗?那我们可以使用aop吗?
注意是不可以的,因为我们这里想要达到的效果是,当用户登录成功,之后才进行发送短信或者发送邮件的操作,而aop切面只能针对某个方法进行调用的时候来进行织入通知,是实现不了登录成功才执行的这个效果的。
此时如何解决这个问题呢?
我们先来看看我们正常的login逻辑:
而我们上面的写法是这样的:
可以看到发送短信的这个功能耦合到了login方法的逻辑中了,我们有没有一种机制能够像这样解决它:
如图所示,我们可以通过这种监听机制来解决这个问题,来达到解耦合的效果,那么代码我们如何实现呢?
Spring中的事件监听
Spring中通过ApplicationEvent
类和ApplicationListener
接口提供了事件处理的机制,如果一个bean实现了ApplicationListener
接口并且放到了IOC容器当中,每次发布一个ApplicationEvent
事件到ApplicatioinContext
,那么这个ApplicationListener
监听者就能被通知到。
我们如何自定义一个事件呢?
基于SpringBoot简单实现一个案例:
springboot的版本如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/>
</parent>
引入的依赖如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
1、首先定义要给登录成功的事件
// 定义一个登录成功的事件,继承ApplicationContext类,因为该类中只有一个有参构造,所以这里我们要调用父类的有参构造并传入参数
public class LoginOKEvent extends ApplicationEvent {
public LoginOKEvent(Object source) {
super(source);
}
}
2、定义要给事件监听器
// 监听LoginOKEvent这个事件
@Service
public class LoginListener implements ApplicationListener<LoginOKEvent> {
// 自定义发送邮件的逻辑
public void sendEmail(String address, String content) {
System.out.println("发送邮件的逻辑。。。");
}
// 如果监听到了LoginOKEvent这个事件,就会自己调用这个方法
@Override
public void onApplicationEvent(LoginOKEvent event) {
this.sendEmail("aaa@qq.com","haha");
}
}
3、在登录校验成功之后发布登录成功的事件
@Service
public class LoginService {
@Autowired
private ApplicationContext applicationContext;
public void login(String username, String password) {
// 登录校验的逻辑
System.out.println("用户名密码正确");
// 登录成功发布登录成功的事件
applicationContext.publishEvent(new LoginOKEvent(this));
}
}
4、测试方法
@SpringBootTest
public class Test {
@Autowired
private LoginService loginService;
@org.junit.jupiter.api.Test
public void testSpringEvent() {
loginService.login("张三","123");
}
}
执行结果
此时就实现了我们上图的效果
如果我们就算更换逻辑,只要添加新的监听者就行,不用修改原来的代码了。
比如,实现在登录成功之后给用户发送短信:
// 监听LoginOKEvent这个事件
@Service
public class LoginListener implements ApplicationListener<LoginOKEvent> {
// 自定义发送邮件的逻辑
public void sendMessage() {
System.out.println("发送短信的逻辑。。。");
}
// 如果监听到了LoginOKEvent这个事件,就会自己调用这个方法
@Override
public void onApplicationEvent(LoginOKEvent event) {
this.sendEmail();
}
}
这种写法叫做:“以增量的方式应对变化的需求”,而不是去修改已有的代码。假设有B接口调用了C接口,你修改了C接口,那么B接口可能业务结果就错了,此时调用B接口的A接口也可能受到影响,是连锁反应。所以,一般我们都提倡“对扩展开放,对修改关闭”的原则。
在上面的监听者的代码中还是有一些麻烦,因为我们仅仅是为了监听事件然后回调方法,但是还需要实现一个类就显得有些麻烦了,这里Spring提供了注解的写法
// 监听LoginOKEvent这个事件
@Service
public class LoginListener {
// 自定义发送邮件的逻辑
@EventListener(LoginOKEvent)
public void sendMessage() {
System.out.println("发送短信的逻辑。。。");
}
}
然后Spring当中的事件监听基本就基于Springboot说完了,这里还存在一个小的问题,就是这里我们是采用同步的方式进行执行的发布的事件然后监听者执行方法,我们为了提高性能,不阻塞主线程,可以考虑异步的实现方式,例如:
@Service
public class LoginService {
@Autowired
private ApplicationContext applicationContext;
public void login(String username, String password) {
// 登录校验的逻辑
System.out.println("用户名密码正确");
// 创建线程使用异步的方式来发布事件
new Thread(() ->{
// 登录成功发布登录成功的事件
applicationContext.publishEvent(new LoginOKEvent(this));
}).start();
}
}