SpringBoot AOP 自定義註解異步監聽方式實現日誌記錄(附源碼)

一. 功能簡介

本文主要記錄如何使用 註解+aop切面+異步監聽 的方式來實現日誌記錄功能。java

主要記錄的信息有: 操做人,操做IP,方法名,參數,消耗時間,日誌類型,操做類型(操做日誌和異常日誌)以及增刪改查記錄,操做時間等。git

主要流程:github

AOP切面獲得請求數據 -> 發佈監聽事件 -> 異步監聽日誌入庫web

二. 項目結構

三. 項目實戰

1.引入依賴

<dependency>

    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
複製代碼

2.自定義註解

主要標註日誌的具體用處也就是具體操做spring

package com.xd.pre.log;

import java.lang.annotation.*;

//元註解,定義註解被保留策略,通常有三種策略
//一、RetentionPolicy.SOURCE 註解只保留在源文件中,在編譯成class文件的時候被遺棄
//二、RetentionPolicy.CLASS 註解被保留在class中,可是在jvm加載的時候北歐拋棄,這個是默認的聲明週期
//三、RetentionPolicy.RUNTIME 註解在jvm加載的時候仍被保留
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})  //定義了註解聲明在哪些元素以前
@Documented
public @interface SysLog {
    //定義成員
    String descrption() default "" ;//描述
}
複製代碼

3.AOP切面類

AOP切面類是最主要的,能夠使用自定義註解或針對包名實現AOP加強。設計模式

1)這裏實現了對自定義註解的切點,對使用了自定義註解的方法進行AOP切面處理;瀏覽器

2)對方法運行時間進行監控;bash

3)對方法名,參數名,參數值,對日誌描述以及異常信息的優化處理;mybatis

4)發佈監聽事件,日誌異步入庫app

在方法上增長@Aspect 註解聲明切面,使用@Pointcut 註解定義切點,標記方法。

package com.xd.pre.log;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.URLUtil;
import cn.hutool.extra.servlet.ServletUtil;
import com.xd.pre.security.PreUser;
import com.xd.pre.security.util.SecurityUtil;
import com.xd.pre.utils.LogUtil;
import com.xd.pre.utils.R;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Objects;

/**
 * @Classname SysLogAspect
 * @Description 系統日誌切面
 * @Author 李號東 lihaodongmail@163.comgit reset --merge
 * @Date 2019-04-22 23:52
 * @Version 1.0
 * ①切面註解獲得請求數據 -> ②發佈監聽事件 -> ③異步監聽日誌入庫
 */
@Slf4j
@Aspect
@Component
public class SysLogAspect {


    private com.xd.pre.domain.SysLog sysLog = new com.xd.pre.domain.SysLog();

    
    /**
     * 事件發佈是由ApplicationContext對象管控的,咱們發佈事件前須要注入ApplicationContext對象調用publishEvent方法完成事件發佈
     **/
    @Autowired
    private ApplicationContext applicationContext;

    /***
     * 定義controller切入點攔截規則,攔截SysLog註解的方法
     */
    @Pointcut("@annotation(com.xd.pre.log.SysLog)")
    public void sysLogAspect() {

    }

    /***
     * 攔截控制層的操做日誌
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Before(value = "sysLogAspect()")
    public void recordLog(JoinPoint joinPoint) throws Throwable {

        // 開始時間
        long beginTime = Instant.now().toEpochMilli();
        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        PreUser securityUser = SecurityUtil.getUser();
        sysLog.setUserName(securityUser.getUsername());
        sysLog.setActionUrl(URLUtil.getPath(request.getRequestURI()));
        sysLog.setStartTime(LocalDateTime.now());
        sysLog.setRequestIp(ServletUtil.getClientIP(request));
        sysLog.setRequestMethod(request.getMethod());
        sysLog.setUa(request.getHeader("user-agent"));
        //訪問目標方法的參數 可動態改變參數值
        Object[] args = joinPoint.getArgs();
        //獲取執行的方法名
        sysLog.setActionMethod(joinPoint.getSignature().getName());
        // 類名
        sysLog.setClassPath(joinPoint.getTarget().getClass().getName());
        sysLog.setActionMethod(joinPoint.getSignature().getName());
        sysLog.setFinishTime(LocalDateTime.now());
        // 參數
        sysLog.setParams(Arrays.toString(args));
        sysLog.setDescription(LogUtil.getControllerMethodDescription(joinPoint));
        long endTime = Instant.now().toEpochMilli();
        sysLog.setConsumingTime(endTime - beginTime);
    }

    /**
     * 返回通知
     * @param ret
     * @throws Throwable
     */
    @AfterReturning(returning = "ret", pointcut = "sysLogAspect()")
    public void doAfterReturning(Object ret) {
        // 處理完請求,返回內容
        R r = Convert.convert(R.class, ret);
        if (r.getCode() == 200){
            // 正常返回
            sysLog.setType(1);
        } else {
            sysLog.setType(2);
            sysLog.setExDetail(r.getMsg());
        }
        // 發佈事件
        applicationContext.publishEvent(new SysLogEvent(sysLog));
    }

    /**
     * 異常通知
     * @param e
     */
    @AfterThrowing(pointcut = "sysLogAspect()",throwing = "e")
    public void doAfterThrowable(Throwable e){
        // 異常
        sysLog.setType(2);
        // 異常對象
        sysLog.setExDetail(LogUtil.getStackTrace(e));
        // 異常信息
        sysLog.setExDesc(e.getMessage());
        // 發佈事件
        applicationContext.publishEvent(new SysLogEvent(sysLog));
    }

}
複製代碼

目前使用: 前置通知,後置通知,異常通知

事件發佈是由ApplicationContext對象管控的,咱們發佈事件前須要注入ApplicationContext對象調用publishEvent方法完成事件發佈

4.日誌實體類

主要記錄日誌的詳細信息

package com.xd.pre.domain;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
import java.io.Serializable;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

/**
 * <p>
 * 系統日誌
 * </p>
 *
 * @author lihaodong
 * @since 2019-04-27
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class SysLog implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主鍵
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    /**
     * 操做IP
     */
    private String requestIp;

    /**
     * 操做類型 1 操做記錄 2異常記錄
     */
    private Integer type;

    /**
     * 操做人ID
     */
    private String userName;

    /**
     * 操做描述
     */
    private String description;

    /**
     * 請求方法
     */
    private String actionMethod;

    /**
     * 請求url
     */
    private String actionUrl;

    /**
     * 請求參數
     */
    private String params;

    /**
     * 瀏覽器
     */
    private String ua;

    /**
     * 類路徑
     */
    private String classPath;

    /**
     * 請求方法
     */
    private String requestMethod;

    /**
     * 開始時間
     */
    private LocalDateTime startTime;

    /**
     * 完成時間
     */
    private LocalDateTime finishTime;

    /**
     * 消耗時間
     */
    private Long consumingTime;

    /**
     * 異常詳情信息 堆棧信息
     */
    private String exDetail;

    /**
     * 異常描述 e.getMessage
     */
    private String exDesc;

}
複製代碼

5.ApplicationEvent&Listener完成業務解耦

ApplicationEvent以及Listener是Spring爲咱們提供的一個事件監聽、訂閱的實現,內部實現原理是觀察者設計模式,設計初衷也是爲了系統業務邏輯之間的解耦,提升可擴展性以及可維護性。事件發佈者並不須要考慮誰去監聽,監聽具體的實現內容是什麼,發佈者的工做只是爲了發佈事件而已。

其實生活中有不少這樣的例子: 咱們在平時常見的比賽中,裁判員或者主持人給咱們開始的信號,也就是給咱們發佈了一個開始的事件,而參賽的雙方人員都在監聽着這個事件,一旦事件發佈後雙方人員就開始拼命努力贏得比賽。而裁判或者主持人並不關心你比賽的過程,只是給你發佈事件你執行就能夠了

系統日誌事件

package com.xd.pre.log;

import com.xd.pre.domain.SysLog;
import org.springframework.context.ApplicationEvent;

/**
 * @Classname SysLogEvent
 * @Description 系統日誌事件
 * @Author 李號東 lihaodongmail@163.com
 * @Date 2019-04-28 11:34
 * @Version 1.0
 */
public class SysLogEvent extends ApplicationEvent {

    public SysLogEvent(SysLog source) {
        super(source);
    }
}
複製代碼

咱們自定義事件SysLogEvent繼承了ApplicationEvent,繼承後必須重載構造函數,構造函數的參數能夠任意指定,其中source參數指的是發生事件的對象,通常咱們在發佈事件時使用的是this關鍵字代替本類對象,而sysLog參數是咱們自定義的註冊用戶對象,該對象能夠在監聽內被獲取。

異步監聽日誌事件

package com.xd.pre.log;

import com.xd.pre.domain.SysLog;
import com.xd.pre.service.ISysLogService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

/**
 * @Classname SysLogListener
 * @Description 註解形式的監聽 異步監聽日誌事件
 * @Author 李號東 lihaodongmail@163.com
 * @Date 2019-04-28 11:34
 * @Version 1.0
 */
@Slf4j
@Component
public class SysLogListener {

    @Autowired
    private ISysLogService sysLogService;

    @Async
    @Order
    @EventListener(SysLogEvent.class)
    public void saveSysLog(SysLogEvent event) {
        SysLog sysLog = (SysLog) event.getSource();
        // 保存日誌
        sysLogService.save(sysLog);
    }
}
複製代碼

咱們只須要讓咱們的監聽類被Spring所管理便可,在咱們用戶註冊監聽實現方法上添加@EventListener註解,該註解會根據方法內配置的事件完成監聽。

6.具體用法

只須要在你控制層要記錄日誌的方法上加上@SysLog註解便可

/**
 * 獲取用戶列表集合
 *
 * @param page
 * @param userDTO
 * @return
 */
@SysLog(descrption = "查詢用戶集合")
@GetMapping
@PreAuthorize("hasAuthority('sys:user:view')")
public R getList(Page page, UserDTO userDTO) {
    return R.ok(userService.getUsersWithRolePage(page, userDTO));
}
複製代碼

源碼地址 碼雲: gitee.com/li_haodong/…
GitHub: github.com/LiHaodong88…

相關文章
相關標籤/搜索