Spring Event事件通知

Spring的事件通知机制是一项很有用的功能,使用事件机制可将相互耦合的代码解耦,从而方便功能的开发。

1.入门案例

1.1环境准备

新建一个SpringBoot的项目,导入web的依赖,编写一个controller接口:

package com.zys.springboottestexample.controller;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
@RequestMapping("/user")
public class UserController {

    @PostMapping("/save")
    public void save(Map map) {
        System.out.println(map);
    }

}

1.2使用Spring Event

1)使用说明

使用用事件需要以下的几个步骤:

第一:定义事件,继承ApplicationEvent

第二:定义监听,实现ApplicationListener接口或添加注解@EventListener

第三:发布事件,调用ApplicationEventPublisher.publishEvent()或ApplicationContext.publishEvent()

2)定义事件

package com.zys.springboottestexample.event;

import org.springframework.context.ApplicationEvent;

// 定义一个事件
public class EventDemo extends ApplicationEvent {
    
    private String message;

    public EventDemo(Object source, String message) {
        super(source);
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

3)定义监听

package com.zys.springboottestexample.listener;

import com.zys.springboottestexample.entity.EventDemo;
import org.springframework.context.ApplicationListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

// 定义一个事件监听者
@Component
public class EventDemoListener implements ApplicationListener<EventDemo> {

    @Override
    public void onApplicationEvent(EventDemo event) {
        System.out.println("当前线程:" + Thread.currentThread().getId());
        System.out.println("receiver " + event.getMessage());
    }
}

4)发布事件

package com.zys.springboottestexample.service;

import com.zys.springboottestexample.entity.EventDemo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;

// 事件发布的方法
@Component
public class EventPublishService {

    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    public void publish(String message) {
        EventDemo demo = new EventDemo(this, message);
        applicationEventPublisher.publishEvent(demo);
    }
}

5)触发事件

当事件发布后,在需要的地方就可以触发事件了。在上述UserController中触发:

    @Autowired
    private EventPublishService eventPublishService;

    @PostMapping("/save")
    public void save(Map map) {
        System.out.println(map);
        System.out.println("当前线程:" + Thread.currentThread().getId());
        eventPublishService.publish("添加成功");
    }

6)测试。访问localhost:8080/user/save,在控制台打印了信息

7)异步处理

上述看到打印结果分析,处理接口的线程和事件监听使用的是同一个线程。一般会使用异步的方式。

第一步:首先在事件监听的方法上添加异步的注解@Async

第二步:然后在启动类上启用异步@EnableAsync

第三步:重启项目,再次访问上述接口,控制台打印的线程id就不一样了

2.实际应用

2.1应用场景

当然在实际应用中,Spring Event因解耦的特性也显得格外重要。比如现有一个请假申请的方法,在申请保存到数据库的同时需要给上级领导发送系统和邮件通知。当然可以在保存申请的代码后面假设这些操作,但是这样的代码违反了设计模式的多项原则:单一职责原则、迪米特法则、开闭原则。也就是说,比如将来评论添加成功之后还需要有发送短信通知,这时又要去修改存申请代码才能符合需求。若使用了事件通知机制,则无需修改原有功能,只需在发布通知功能中调用短信发送功能即可。

2.2自定义日志收集的starter

本章节通过自定义一个starter,名为log-spring-boot-starter,用来拦截用户请求并收集操作日志信息,收集的信息会通过监听器返回给使用者,使用者再获取。

源码:https://github.com/zhongyushi-git/zxh-starter-collection.git

2.2.1开发步骤

具体步骤如下:

1)定义日志对象LogDTO

2)定义日志操作事件类LogEvent

3)定义@Log注解

4)定义切面类LogAspect

5)在切面类LogAspect中定义切点,拦截Controller中添加@Log注解的方法

6)在切面类LogAspect中定义前置通知,在前置通知方法中收集操作日志相关信息封装为LogDTO对象并保存到ThreadLocal中

7)在切面类LogAspect中定义后置通知,在后置通知方法中通过ThreadLocal获取LogDTO并继续设置其他的操作信息到LogDTO

8)在切面类LogAspect的后置通知方法中发布事件LogEvent

9)定义监听器LogListener,监听日志发布事件LogEvent

10)定义配置类LogAutoConfiguration,用于自动配置切面LogAspect对象。在配置类有一个属性sys.log.enabled,表示是否启用日志收集,值true启用,值false不启用,若不配置时也生效。

11)定义starter所需的META-INF/spring.factories文件,配置自动配置类

2.2.2说明

1)使用@Log注解时,参数value是接口的描述,type是日志的类型,1表示操作日志,2表示登录日志。

2)代码中的自动配置类的关键注解说明:

注解名称 描述

@ConditionalOnWebApplication  

只有当前项目是Web项目的条件下生效

@ConditionalOnProperty       

指定的属性是否有指定的值,通过havingValue与配置文件中的值对比,返回为true则配置类生效,反之失效

@ConditionalOnMissingBean        

用来修饰bean,当注册此bean时,会检查是否已经注册过此Bean,若注册过就不会再次注册,若没有注册过则进行注册,保证此bean只有一个。

2.3使用日志收集的starter

1)新建SpringBoot的项目,导入web和此starter依赖

 <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
  </dependency>      
  <dependency>
      <groupId>com.zxh.boot</groupId>
      <artifactId>log-spring-boot-starter</artifactId>
      <version>1.0-SNAPSHOT</version>
    </dependency>

2)定义LogService类,用于保存日志信息

package com.zys.springboottestexample.service;

import com.zxh.boot.log.entity.LogDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class LogService {

    public void saveLog(LogDTO logDTO){
        log.info(logDTO.toString());
        //保存到数据库
        
    }
}

这里只是打印,实际中将对象按需求存入数据库即可

3)定义配置类LogConfiguration,用于初始化监听

package com.zys.springboottestexample.config;

import com.zxh.boot.log.listener.LogListener;
import com.zys.springboottestexample.service.LogService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 日志配置类
 */
@Configuration
public class LogConfiguration {

    /**
     * 初始化监听器,
     * @param logService
     * @return
     */
    @Bean
    @ConditionalOnMissingBean
    public LogListener logListener(LogService logService){
        //函数式接口
        return new LogListener(logDTO -> logService.saveLog(logDTO));
    }
}

当日志事件发布后,会在Log监听器中进行监听,并调用consumer.accept()方法,而在初始化监听器中函数式接口又指明了调用的方法,则最终会自动调用logService.saveLog()方法。

4)创建UserController类,定义一个接口,添加日志注解

package com.zys.springboottestexample.controller;

import com.zxh.boot.log.annotation.Log;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
public class UserController {

    @GetMapping("/save")
    @Log("用户添加")
    public String save(String name) {
        int t=10/0;
        return "你好啊";
    }
}

5)启动项目,访问localhost:8080/user/save?name=123即可在控制台看到打印的日志对象

从控制台可以看出,虽然在代码中故意制造了异常,但仍然有日志信息,那么这些日志被记录到数据库,就可查询错误的信息,这就体现了日志的重要性。

6)若不再使用此starter,除了删除其依赖外,还可以直接在配置文件中配置:

sys.log.enabled=false

那么即使加了@Log注解,也不会生效。

7)获取登录用户信息

可以通过设置请求头header让日志获取登录用户信息,需要设置的有两个,分别是userId和userName,例如:

就是这么简单,你学废了吗?感觉有用的话,给笔者点个赞吧 !
原文地址:https://www.cnblogs.com/zys2019/p/15020331.html