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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM