我使用Spring AOP實現了用戶操作日誌功能

来源:https://www.cnblogs.com/Fzeng/archive/2022/05/21/16294977.html
-Advertisement-
Play Games

我使用Spring AOP實現了用戶操作日誌功能 今天答辯完了,復盤了一下系統,發現還是有一些東西值得拿出來和大家分享一下。 需求分析 系統需要對用戶的操作進行記錄,方便未來溯源 首先想到的就是在每個方法中,去實現記錄的邏輯,但是這樣做肯定是不現實的,首先工作量大,其次違背了軟體工程設計原則(開閉原 ...


我使用Spring AOP實現了用戶操作日誌功能

今天答辯完了,復盤了一下系統,發現還是有一些東西值得拿出來和大家分享一下。

需求分析

系統需要對用戶的操作進行記錄,方便未來溯源

首先想到的就是在每個方法中,去實現記錄的邏輯,但是這樣做肯定是不現實的,首先工作量大,其次違背了軟體工程設計原則(開閉原則)

這種需求顯然是對代碼進行增強,首先想到的是使用 SpringBoot 提供的 AOP 結合註解的方式來實現

功能實現

1、 需要一張記錄日誌的 Log 表

image-20220521134019891

導出的 sql 如下:

-- mcams.t_log definition

CREATE TABLE `t_log` (
  `log_id` int NOT NULL AUTO_INCREMENT COMMENT '日誌編號',
  `user_id` int NOT NULL COMMENT '操作人id',
  `operation` varchar(128) NOT NULL COMMENT '用戶操作',
  `method` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '操作的方法',
  `params` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '方法的參數',
  `ip` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '用戶的ip',
  `create_time` timestamp NULL DEFAULT NULL COMMENT '操作時間',
  `cost_time` int DEFAULT NULL COMMENT '花費時間',
  PRIMARY KEY (`log_id`)
) ENGINE=InnoDB AUTO_INCREMENT=189 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

2、我使用的是 Spring Boot 所以需要引入 spring aop 的 starter

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

如果是使用 spring 框架的,引入 spring-aop 即可

3、Log 實體類

package com.xiaofengstu.mcams.web.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.time.LocalDateTime;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Getter;
import lombok.Setter;

/**
 * <p>
 * 
 * </p>
 *
 * @author fengzeng
 * @since 2022-05-21
 */
@Getter
@Setter
@TableName("t_log")
public class TLog implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "log_id", type = IdType.AUTO)
    private Integer logId;

    /**
     * 操作人id
     */
    @TableField("user_id")
    private Integer userId;

    /**
     * 用戶操作
     */
    @TableField("operation")
    private String operation;


    @TableField("method")
    private String method;

    @TableField("params")
    private String params;

    @TableField("ip")
    private String ip;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @TableField("create_time")
    private LocalDateTime createTime;

    @TableField("cost_time")
    private Long costTime;


}

需要 lombok 插件(@getter && @setter 註解)

4、ILog 註解

package com.xiaofengstu.mcams.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @Author FengZeng
 * @Date 2022-05-21 00:48
 * @Description TODO
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ILog {
  String value() default "";
}

5、切麵類 LogAspect

package com.xiaofengstu.mcams.aspect;

import com.xiaofengstu.mcams.annotation.ILog;
import com.xiaofengstu.mcams.util.ThreadLocalUtils;
import com.xiaofengstu.mcams.web.entity.TLog;
import com.xiaofengstu.mcams.web.service.TLogService;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
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.lang.reflect.Method;
import java.time.LocalDateTime;

/**
 * @Author FengZeng
 * @Date 2022-05-21 00:42
 * @Description TODO
 */
@Aspect
@Component
@RequiredArgsConstructor
public class LogAspect {
  private final TLogService logService;

  @Pointcut("@annotation(com.xiaofengstu.mcams.annotation.ILog)")
  public void pointcut() {

  }

  @Around("pointcut()")
  public Object around(ProceedingJoinPoint point) {
    Object result = null;
    long beginTime = System.currentTimeMillis();

    try {
      result = point.proceed();
    } catch (Throwable e) {
      e.printStackTrace();
    }

    long costTime = System.currentTimeMillis() - beginTime;

    saveLog(point, costTime);

    return result;
  }

  private void saveLog(ProceedingJoinPoint point, long costTime) {
    // 通過 point 拿到方法簽名
    MethodSignature methodSignature = (MethodSignature) point.getSignature();
    // 通過方法簽名拿到被調用的方法
    Method method = methodSignature.getMethod();

    TLog log = new TLog();
    // 通過方法區獲取方法上的 ILog 註解
    ILog logAnnotation = method.getAnnotation(ILog.class);
    if (logAnnotation != null) {
      log.setOperation(logAnnotation.value());
    }

    String className = point.getTarget().getClass().getName();
    String methodName = methodSignature.getName();
    log.setMethod(className + "." + methodName + "()");

    // 獲取方法的參數
    Object[] args = point.getArgs();
    LocalVariableTableParameterNameDiscoverer l = new LocalVariableTableParameterNameDiscoverer();
    String[] parameterNames = l.getParameterNames(method);
    if (args != null && parameterNames != null) {
      StringBuilder param = new StringBuilder();
      for (int i = 0; i < args.length; i++) {
        param.append(" ").append(parameterNames[i]).append(":").append(args[i]);
      }
      log.setParams(param.toString());
    }

    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    HttpServletRequest request = attributes.getRequest();

    log.setIp(request.getRemoteAddr());

    log.setUserId(ThreadLocalUtils.get());
    log.setCostTime(costTime);
    log.setCreateTime(LocalDateTime.now());

    logService.save(log);
  }

}

因為我使用的是 Mybatis-plus,所以 logService.save(log);是 mybatis-plus 原生的 save operation

這步其實就是把 log 插入到資料庫

6、使用

只需要在方法上加上 @ILog 註解,並設置它的 value 即可(value 就是描述當前 method 作用)

package com.xiaofengstu.mcams.web.controller;


import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.xiaofengstu.mcams.annotation.ILog;
import com.xiaofengstu.mcams.dto.BasicResultDTO;
import com.xiaofengstu.mcams.enums.RespStatusEnum;
import com.xiaofengstu.mcams.web.entity.TDept;
import com.xiaofengstu.mcams.web.service.TDeptService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * <p>
 * 前端控制器
 * </p>
 *
 * @author fengzeng
 * @since 2022-05-07
 */
@RestController
@RequestMapping("/web/dept")
@RequiredArgsConstructor
public class TDeptController {

  private final TDeptService deptService;

  @ILog("獲取部門列表")
  @GetMapping("/list")
  public BasicResultDTO<List<TDept>> getDeptListByCampId(@RequestParam("campusId") Integer campusId) {
    return new BasicResultDTO(RespStatusEnum.SUCCESS, deptService.list(new QueryWrapper<TDept>().eq("campus_id", campusId)));
  }

  @ILog("通過角色獲取部門列表")
  @GetMapping("/listByRole")
  public BasicResultDTO<List<TDept>> getDeptListByRole() {
    return new BasicResultDTO<>(RespStatusEnum.SUCCESS, deptService.listByRole());
  }

}

資料庫:

image-20220521135144910

總結

如果要對現有代碼進行功能擴展,使用 AOP + 註解不妨為一種優雅的方式

對 AOP 不熟悉的小伙伴,可以深入瞭解一下,畢竟是 spring 最重要的特性之一。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 摘要:本篇我們將以分析歷史股價為例,介紹怎樣從文件中載入數據,以及怎樣使用NumPy的基本數學和統計分析函數、學習讀寫文件的方法,並嘗試函數式編程和NumPy線性代數運算,來學習NumPy的常用函數。 一、文件讀入 :讀寫文件是數據分析的一項基本技能 CSV(Comma-Separated Valu ...
  • 轉載請註明出處❤️ 作者:IT小學生蔡坨坨 原文鏈接:https://www.caituotuo.top/a3a8d0c0.html 大家好,我是IT小學生蔡坨坨。 今天,我們來聊聊介面自動化測試是什麼?如何開始?介面自動化測試框架怎麼做? 自動化測試 自動化測試,這幾年行業內的熱詞,也是測試人員進 ...
  • 知識回顧 Bean的創建過程會經歷getBean,doGetBean,createBean,doCreateBean,然後Bean的創建又會經歷實例化,屬性填充,初始化。 在實例化createInstance時大致可以分為三種方式進行實例化: 使用Supplier 進行實例化,通過BeanFacto ...
  • 在上一篇文章《SpringBoot進階教程(七十三)整合elasticsearch 》,已經詳細介紹了關於elasticsearch的安裝與使用,現在主要來看看關於ELK的定義、安裝及使用。 v簡介 1.什麼是ELK? ELK 是elastic公司提供的一套完整的日誌收集以及展示的解決方案,是三個產 ...
  • 強轉int類型會直接對浮點數的小數部分進行截斷(無論是正還是負)。還有一種方法是math.ceil和math.floor。無論是正數還是負數,都遵循:ceil往數軸正方向取整,floor往數軸負方向取整。round原型為round(value, ndigits),可以將一個浮點數取整到固定的小數位。... ...
  • 很多人都喜歡使用黑色的主題樣式,包括我自己,使用了差不多三年的黑色主題,但是個人覺得在進行視窗轉換的時候很廢眼睛。 比如IDEA是全黑的,然後需要看PDF或者WORD又變成白色的了,這樣來回切換導致眼睛很累,畢竟現在網頁以及大部分軟體的界面都是白色的。那麼還是老老實實的使用原來比較順眼的模式吧。 1 ...
  • 本期教程人臉識別第三方平臺為虹軟科技,本文章講解的是人臉識別RGB活體追蹤技術,免費的功能很多可以自行搭配,希望在你看完本章課程有所收穫。 ...
  • 《零基礎學Java》 繪製幾何圖形 Java可以分別使用 Graphics 和 Graphics2D 繪製圖形,Graphics類 使用不同的方法繪製不同的圖形(drawLine()方法可f以繪製線、drawRect()方法用於繪製矩形、drawOval()方法用於繪製橢圓形)。 Graphics類 ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...