我使用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实现了用户操作日志功能的更多相关文章

  1. Spring AOP的实现记录操作日志

    适用场景: 记录接口方法的执行情况,记录相关状态到日志中. 注解类:LogTag.java package com.lichmama.spring.annotation; import java.la ...

  2. 利用Spring AOP和自定义注解实现日志功能

    Spring AOP的主要功能相信大家都知道,日志记录.权限校验等等. 用法就是定义一个切入点(Pointcut),定义一个通知(Advice),然后设置通知在该切入点上执行的方式(前置.后置.环绕等 ...

  3. spring aop切面编程实现操作日志步骤

    1.在spring-mvc.xml配置文件中打开切面开关: <aop:aspectj-autoproxy proxy-target-class="true"/> 注意: ...

  4. 基于NopCommerce的开发框架——缓存、网站设置、系统日志、用户操作日志

    最近忙于学车,抽时间将Nop的一些公用模块添加进来,反应的一些小问题也做了修复.另外有园友指出Nop内存消耗大,作为一个开源电商项目,性能方面不是该团队首要考虑的,开发容易,稳定,代码结构清晰简洁也是 ...

  5. 利用Spring AOP切面对用户访问进行监控

    开发系统时往往需要考虑记录用户访问系统查询了那些数据.进行了什么操作,尤其是访问重要的数据和执行重要的操作的时候将数记录下来尤显的有意义.有了这些用户行为数据,事后可以以用户为条件对用户在系统的访问和 ...

  6. 微软企业库5.0 学习之路——第九步、使用PolicyInjection模块进行AOP—PART4——建立自定义Call Handler实现用户操作日志记录

    在前面的Part3中, 我介绍Policy Injection模块中内置的Call Handler的使用方法,今天则继续介绍Call Handler——Custom Call Handler,通过建立 ...

  7. spring AOP自定义注解方式实现日志管理

    今天继续实现AOP,到这里我个人认为是最灵活,可扩展的方式了,就拿日志管理来说,用Spring AOP 自定义注解形式实现日志管理.废话不多说,直接开始!!! 关于配置我还是的再说一遍. 在appli ...

  8. Java Spring+Mysql+Mybatis 实现用户登录注册功能

    前言: 最近在学习Java的编程,前辈让我写一个包含数据库和前端的用户登录功能,通过看博客等我先是写了一个最基础的servlet+jsp,再到后来开始用maven进行编程,最终的完成版是一个 Spri ...

  9. springAOP记录用户操作日志

    项目已经开发完成,需要加用户操作日志,如果返回去加也不太现实,所以使用springAOP来完成比较合适. 注解工具类: @Retention(RetentionPolicy.RUNTIME) @Tar ...

随机推荐

  1. CSDN博客步骤:

    在SCDN看到喜欢的文章想转载又嫌一个一个敲太麻烦,干脆直接收藏.但有时候作者把原文章删除或设置为私密文章后又看不了.所以还是转载来的好.这篇博文为快速转载博客的方法,亲测有效,教程如下. 原博客原址 ...

  2. 前端性能优化(Application Cache篇)

    正巧看到在送书,于是乎找了找自己博客上记录过的一些东西来及其无耻的蹭书了~~~ 小广告:更多内容可以看我的博客 之前在segmentfault上刷问题看到一个关于manifest的问题,很好奇就研究了 ...

  3. 用 JWT 实现小程序本地用户标识

    panda-chat-room 继上节「理解小程序 session」 ,本节我们以 jsonwebtoken 来实现小程序端的用户状态标识.如果你对小程序用户登录流程及 session 管理还有些疑惑 ...

  4. Python:爬取全国各省疫情数据并在地图显示

    代码: import requests import pymysql import json from pyecharts import options as opts from pyecharts. ...

  5. scrapy爬虫简单案例(简单易懂 适合新手)

    爬取所有的电影名字,类型,时间等信息 1.准备工作 爬取的网页 https://www.ddoutv.com/f/27-1.html 创建项目 win + R 打开cmd输入 scrapy start ...

  6. Linux上部署net6应用

    前言 ​ .net6都出来了,作为一名.net搬砖工却一直都在windows下部署应用,还未尝试过linux环境下部署应用.参考福禄网络研发团队的相关博客,学习一下如何将应用部署到linux系统. . ...

  7. php实验一专属跳转博文

    今天完成了php关于设计个人博客主页的实验一作业. 这是php实验一作业中博客的跳转链接页.  

  8. Java学习day13

    泛型类格式: 修饰符 class 类名<类型>{ } 常用T.E.K.V等形式的参数表示泛型 使用方式与C++的类模板相似,在创建对象时要明确数据类型 泛型方法定义格式: 修饰符<类 ...

  9. 基于Kubernetes构建企业Jenkins master/slave CI/CD平台

    搭建平台目的: k8s中搭建jenkins master/slave架构,解决单jenkins执行效率低,资源不足等问题(jenkins master 调度任务到 slave上,并发执行任务,提升任务 ...

  10. MySQL进阶之表的增删改查

    我的小站 修改表名 ALTER TABLE student RENAME TO stu; TO可以省略. ALTER TABLE 旧表名 RENAME 新表名; 此语句可以修改表的名称,其实一般我们在 ...