我使用Spring AOP实现了用户操作日志功能
今天答辩完了,复盘了一下系统,发现还是有一些东西值得拿出来和大家分享一下。
需求分析
系统需要对用户的操作进行记录,方便未来溯源
首先想到的就是在每个方法中,去实现记录的逻辑,但是这样做肯定是不现实的,首先工作量大,其次违背了软件工程设计原则(开闭原则)
这种需求显然是对代码进行增强,首先想到的是使用 SpringBoot 提供的 AOP 结合注解的方式来实现
功能实现
1、 需要一张记录日志的 Log 表

导出的 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());
- }
- }
数据库:

总结
如果要对现有代码进行功能扩展,使用 AOP + 注解不妨为一种优雅的方式
对 AOP 不熟悉的小伙伴,可以深入了解一下,毕竟是 spring 最重要的特性之一。
到此这篇关于使用Spring AOP实现了用户操作日志功能的文章就介绍到这了,更多相关Spring AOP用户操作日志内容请搜索w3xue以前的文章或继续浏览下面的相关文章希望大家以后多多支持w3xue!