• 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,例如:

    就是这么简单,你学废了吗?感觉有用的话,给笔者点个赞吧 !
  • 相关阅读:
    Java vs C++ 基础异同比较
    2. 数组
    1. 数据结构概述
    动态规划专题:LeetCode 完全平方数
    动态规划专题:LeetCode 乘积最大子数组
    动态规划专题:LeetCode连续数列
    动态规划专题:LeetCode 按摩师
    3. 无重复字符的最长子串
    字典树(前缀树/后缀树)
    10. 排序算法思想概述及总结(精华)
  • 原文地址:https://www.cnblogs.com/zys2019/p/15020331.html
Copyright © 2020-2023  润新知