8

我使用Spring AOP实现了用户操作日志功能

 1 year ago
source link: https://blog.51cto.com/u_15323393/5454211
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

我使用Spring AOP实现了用户操作日志功能

推荐 原创

今天答辩完了,复盘了一下系统,发现还是有一些东西值得拿出来和大家分享一下。

系统需要对用户的操作进行记录,方便未来溯源

首先想到的就是在每个方法中,去实现记录的逻辑,但是这样做肯定是不现实的,首先工作量大,其次违背了软件工程设计原则(开闭原则)

这种需求显然是对代码进行增强,首先想到的是使用 SpringBoot 提供的 AOP 结合注解的方式来实现

1、 需要一张记录日志的 Log 表

我使用Spring AOP实现了用户操作日志功能_程序员

导出的 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 插入到数据库

只需要在方法上加上 @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());
  }

}
我使用Spring AOP实现了用户操作日志功能_spring_02

如果要对现有代码进行功能扩展,使用 AOP + 注解不妨为一种优雅的方式

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK