AOP与日志(下)

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

日志分类

有时候我们所谓的“记录一下日志”,可能有两种含义:

  • 通过@Slf4j打印日志,方便开发人员排查问题(包括统一的接口日志和自己打的log)
  • 对重要接口进行用户行为记录,出现问题时方便管理员追责

一般来说,前者的数据被保存在log文件中,只是面向开发者,普通用户不易阅读,而后者被称为“用户行为日志”,通常还会额外提供查询接口和专门的页面,方便管理员查看操作记录。今天,我们就来聊聊所谓的“用户行为日志”。

代码

之前已经写过很多AOP和枚举的代码了,这里就不再重复介绍,直接展示代码(MyBatis-Plus):

用户日志SQL

CREATE TABLE `sys_user_log` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `module_code` varchar(255) NOT NULL DEFAULT '',
  `type` tinyint(2) NOT NULL,
  `title` varchar(255) NOT NULL DEFAULT '',
  `operator_id` bigint(20) NOT NULL,
  `operate_time` datetime NOT NULL,
  `content` varchar(255) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB CHARSET=utf8mb4;

UserLogDO

@Data
@TableName("sys_user_log")
public class UserLogDO {

    private Long id;

    /**
     * 本次操作的系统模块
     *
     * @see com.bravo.demo.aspect.ModuleEnum
     */
    private String moduleCode;
    /**
     * 操作类型
     *
     * @see com.bravo.demo.aspect.OperationEnum
     */
    private Integer type;
    /**
     * 标题
     */
    private String title;
    /**
     * 操作人
     */
    private Long operatorId;
    /**
     * 操作时间
     */
    private Date operateTime;
    /**
     * 操作内容
     */
    private String content;

}

UserLogDTO

@Data
public class UserLogDTO {

    private Long id;

    private String moduleCode;

    private Integer type;

    private String title;

    private Long operatorId;

    private Date operateTime;

    private String content;

}

UserLogService

public interface UserLogService {

    /**
     * 插入用户操作日志
     *
     * @param userLogDTO
     * @return
     */
    Boolean addSysLog(UserLogDTO userLogDTO);

}

@Service("UserLogService")
public class UserLogServiceImpl implements UserLogService {

    @Autowired
    private UserLogMapper userLogMapper;

    @Override
    public boolean addSysLog(UserLogDTO userLogDTO) {
        UserLogDO userLogDO = new UserLogDO();
        userLogDO.setModuleCode(userLogDTO.getModuleCode());
        userLogDO.setType(userLogDTO.getType());
        userLogDO.setTitle(userLogDTO.getTitle());
        userLogDO.setOperatorId(userLogDTO.getOperatorId());
        userLogDO.setOperateTime(userLogDTO.getOperateTime());
        userLogDO.setContent(userLogDTO.getContent());

        return userLogMapper.insert(userLogDO) > 0;
    }

}

UserLogMapper

public interface UserLogMapper extends BaseMapper<UserLogDO> {
}

核心代码:@UserLog

/**
 * 用户操作日志注解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface UserLog {
	/**
	 * 所属模块名
	 */
	ModuleEnum module();

	/**
	 * 操作标题
	 */
	String title();

	/**
	 * 操作类型
	 */
	OperationEnum type();
}

核心代码:@EnableUserLog

/**
 * 开启用户日志记录(还需要在相关方法上添加{@link UserLog})
 *
 * @author sunting
 * @date 2021-02-11 14:11
 */
@Import(UserLogAspect.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface EnableUserLog {
}

核心代码:OperationEnum

/**
 * 操作类型枚举
 */
@Getter
public enum OperationEnum {
    /**
     * 新建
     */
    ADD(1, "新建"),
    /**
     * 修改
     */
    MODIFY(2, "修改"),
    /**
     * 删除
     */
    DELETE(3, "删除"),
    /**
     * 导入
     */
    IMPORT(4, "导入"),
    /**
     * 导出
     */
    EXPORT(5, "导出");

    private final Integer value;
    private final String operationType;

    OperationEnum(Integer value, String operationType) {
        this.value = value;
        this.operationType = operationType;
    }
    
}

核心代码:ModuleEnum

/**
 * 系统模块枚举类
 * 可以根据自己系统的实际情况增添模块
 */
@Getter
public enum ModuleEnum {
	/**
	 * 课程
	 */
	COURSE("课程"),
	/**
	 * 用户
	 */
	USER("用户"),
	/**
	 * 消息
	 */
	MESSAGE("消息");

	private final String moduleCode;

	ModuleEnum(String moduleCode) {
		this.moduleCode = moduleCode;
	}

}

核心代码:UserLogAspect

/**
 * 操作日志注解切面实现
 */
@Slf4j
@Aspect
public class UserLogAspect {
	// 这次不用RequestContextHolder了,改成直接注入
    @Autowired
    private HttpServletRequest request;
    @Autowired
    private ObjectMapper objectMapper;
    @Autowired
    UserLogService userLogService;


    @Pointcut("@annotation(com.bravo.demo.aspect.UserLog)")
    public void pointcut() {
    }

    @AfterReturning("pointcut()")
    public void afterReturning(JoinPoint point) {
        saveSysUserLog(point);
    }

    private void saveSysUserLog(JoinPoint point) {
        // 获取当前登录用户
        UserInfoDTO userInfoDTO = getUserInfoDTO();

        // 目标方法、以及方法上的@UserLog注解
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        UserLog userLogAnnotation = method.getAnnotation(UserLog.class);
        if (userLogAnnotation == null) {
            return;
        }

        // 收集相关信息并保存
        UserLogDTO userLogDTO = new UserLogDTO();
        userLogDTO.setModuleCode(userLogAnnotation.module().getModuleCode());
        userLogDTO.setContent(getContentJson(point));
        userLogDTO.setTitle(userLogAnnotation.title());
        userLogDTO.setOperatorId(userInfoDTO.getId());
        userLogDTO.setOperateTime(new Date());
        userLogDTO.setType(userLogAnnotation.type().getValue());

        userLogService.addSysLog(userLogDTO);
    }

    private UserInfoDTO getUserInfoDTO() {
        // UserInfoDTO userInfoDTO = (UserInfoDTO) ThreadLocalMap.get(WebConst.USER_INFO_DTO);
		// 模拟从ThreadLocal获取用户信息,关于ThreadLocal请参考小册相关章节
        UserInfoDTO userInfoDTO = new UserInfoDTO();
        userInfoDTO.setId(10086L);
        return userInfoDTO;
    }

    private String getContentJson(JoinPoint point) {
        String requestType = request.getMethod();
        if ("GET".equals(requestType)) {
            // 如果是GET请求,直接返回QueryString(目前没有针对查询操作进行日志记录,先留着吧)
            return request.getQueryString();
        }

        Object[] args = point.getArgs();
        Object[] arguments = new Object[args.length];

        for (int i = 0; i < args.length; i++) {
            // 只打印客户端传递的参数,排除Spring注入的参数,比如HttpServletRequest
            if (args[i] instanceof ServletRequest
                    || args[i] instanceof ServletResponse
                    || args[i] instanceof MultipartFile) {
                continue;
            }
            arguments[i] = args[i];
        }

        try {
            return objectMapper.writeValueAsString(arguments);
        } catch (JsonProcessingException e) {
            log.error("UserLogAspect#getContentJson JsonProcessingException", e);
        }
        return "";
    }
}

测试

@Slf4j
@RestController
public class UserController {

    @UserLog(module = ModuleEnum.USER, title = "批量更新用户", type = OperationEnum.MODIFY)
    @PostMapping("updateBatchUser")
    public Result<Boolean> updateBatchUser(@Validated @RequestBody ValidationList<User> userList) {
        return Result.success(null);
    }

    @UserLog(module = ModuleEnum.USER, title = "新增用户", type = OperationEnum.ADD)
    @PostMapping("insertUser")
    public Result<Boolean> insertUser(@RequestBody User user) {
        return Result.success(null);
    }

}

上面的代码主要提供一个思路,大家可以根据实际需求扩展或改编。

注意:

本文的处理是把所有参数都转为JSON存储,遇到保存文章等大文本数据时会报错。处理办法是:代码中进行长度处理或者调大content的长度(也可以改为text)

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/258081.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ffmpeg windows开发之一(编译安装及入门指南)

一. 源码包下载 下载地址&#xff1a; Download FFmpegDownload FFmpeg 点击more lease&#xff0c;然后下载 二&#xff1a; MSYS2安装 &#xff1a; 下载地址&#xff1a;MSYS2 执行命令&#xff1a;pacman -Syu pacman -S mingw-w64-x86_64-gcc pacman -S mingw-w64-x86_64…

【Spring】14 ApplicationEventPublisherAware 接口

文章目录 1. 简介2. 作用3. 使用3.1 创建并实现接口3.2 配置 Bean 信息3.3 创建启动类3.4 启动3.5 工作流程图 4. 应用场景总结 Spring 框架为开发者提供了丰富的扩展点&#xff0c;其中之一是 Bean 生命周期中的回调接口。本文将专注介绍一个与事件发布相关的接口 Applicatio…

Windows 系统下本地单机搭建 Redis(一主二从三哨兵)

目录 一、Redis环境准备&#xff1a; 1、下载redis 2、Windows下的.msi安装和.zip格式区别&#xff1a; 二、哨兵介绍&#xff1a; 1、一主二从三哨兵理论图&#xff1a; 2.哨兵的主要功能&#xff1a; 3.哨兵用于实现 redis 集群的高可用&#xff0c;本身也是分布式的&…

LeetCode 1901. 寻找峰值 II

一、题目 1、题目描述 一个 2D 网格中的 峰值 是指那些 严格大于 其相邻格子(上、下、左、右)的元素。 给你一个 从 0 开始编号 的 m x n 矩阵 mat &#xff0c;其中任意两个相邻格子的值都 不相同 。找出 任意一个 峰值 mat[i][j] 并 返回其位置 [i,j] 。 你可以假设整个矩阵…

服务端主动给客户端发消息?实战教学:使用Nestjs实现服务端推送SSE

前言 服务端消息推送SSE是常用的服务器消息通信手段&#xff0c;适用于服务器主动给客户端发送消息的场景&#xff0c;例如私信通知&#xff0c;扫描登录等都可以使用SSE实现。SSE的底层原理是客户端与服务端建立 HTTP 长链接。 Nestjs 框架内置了对SSE的支持&#xff0c;本文…

前端性能监控和错误监控

聚沙成塔每天进步一点点 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发…

FreeRTOS信号量学习

目录 一、信号量的特性 1. 信号量的常规操作 2. 信号量跟队列的对比 3. 两种信号量的对比 4. 信号量函数 4.1 创建 4.2 删除 4.3 give/take 5. 使用二进制信号量来同步 队列(queue)可以用于传输数据&#xff1a;在任务之间、任务和中断之间。 有时候我们只需要传递状态&…

外媒发稿最好的宣传方法是什么?大舍传媒

外媒发稿最好的宣传方法是什么&#xff1f; 引言 在如今信息爆炸的时代&#xff0c;外媒发稿的宣传方法至关重要。大舍传媒作为一家业内知名的传媒公司&#xff0c;积累了丰富的经验和成功案例。本文将探讨外媒发稿最好的宣传方法&#xff0c;旨在帮助读者更好地推广自己的信…

Java基础知识回顾

Java基础 一、Java概述 1、Java技术体系平台 类型简介JavaSE 标准版支持面向桌面级的应用JavaEE 企业版支持为企业开发的应用JavaME 小型版运行在移动终端的平台 2、Java重要的特点 面向对象的语言&#xff08;OOP&#xff09; 健壮的语言&#xff0c;具有强类型转换、异常…

MCU为什么上电不启动?

都遇到过这样的问题吧&#xff0c;自信满满的把程序下载到板子上&#xff0c;结果发现MCU居然没启动。 出现这个问题有很多原因&#xff0c;总结为以下五点&#xff1a; 第一&#xff0c;boot引脚电平不对&#xff0c;例如在GD32的MCU上&#xff0c;boot引脚决定了MCU的启动方式…

【pycharm】Pycharm常用快捷键

批量替换是指一次性替换多个文件中的指定内容。在开发过程中&#xff0c;可能会遇到需要替换多个文件中的某个字符串或者某段代码的情况。如果一个一个文件进行替换&#xff0c;那么将会非常耗时和繁琐。 而使用批量替换功能&#xff0c;则可以一次性完成所有文件的替换操作&am…

MyBatis——自定义MyBatis(了解)

1.自定义MyBatis-了解 创建工程&#xff0c;拷贝上一个工程代码&#xff0c;去掉mybatis的依赖&#xff1a; 1.1.MyBatis的核心对象 我们已经通过案例体验到了mybatis的魅力。现在来梳理一下MyBatis运行时的几个对象&#xff0c;我们需要搞清楚他们的作用&#xff0c;进而需要…

java参数校验

引入依赖 <!--参数效验--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency><!--Length参数效验--><dependency><groupId>org.hib…

pycharm手动安装ini插件

pycharm中新增pytest.ini文件时发现&#xff0c;文件的图标不是配置文件的图标 原因是没有安装ini插件 安装插件的方式有很多种&#xff0c;今天通过去官网下载插件&#xff0c;再安装的方式 第一步&#xff1a;去官网搜索&#xff0c;地址是&#xff1a;https://plugins.jet…

【Java 集合】LinkedBlockingQueue

LinkedBlockingQueue, 顾名思义: 基于链表的阻塞队列, 位于 JUC (java.util.concurrent) 下, 是一个线程安全的集合, 其本身具备了 不支持 null 元素: 存入 null 元素会抛出异常固定不限容量: 在不手动设置容量时, 最大可以支持 Integer.MAX_VALUE 个元素, 也就是理论上的无限个…

MapReduce 基础实战

文章目录 第1关&#xff1a;成绩统计第2关&#xff1a;文件内容合并去重 第1关&#xff1a;成绩统计 编程要求 使用MapReduce计算班级每个学生的最好成绩&#xff0c;输入文件路径为/user/test/input&#xff0c;请将计算后的结果输出到/user/test/output/目录下。 测试说明 …

去掉乘法运算的加法移位神经网络架构

[CVPR 2020] AdderNet: Do We Really Need Multiplications in Deep Learning? 代码&#xff1a;https://github.com/huawei-noah/AdderNet/tree/master 核心贡献 用filter与input feature之间的L1-范数距离作为“卷积层”的输出为了提升模型性能&#xff0c;提出全精度梯度…

Python之math模块常用方法汇总

python中math模块常用的方法整理 ceil:取大于等于x的最小的整数值&#xff0c;如果x是一个整数&#xff0c;则返回x copysign:把y的正负号加到x前面&#xff0c;可以使用0 cos:求x的余弦&#xff0c;x必须是弧度 degrees:把x从弧度转换成角度 e:表示一个常量 exp:返回mat…

docker制作php5.4运行环境镜像

1.下载镜像 docker pull centos:7或者在控制面板下 2.运行centos7镜像的容器&#xff0c;edncenos7 是新生成的容器名称 ## --name 新名字 docker run -it --name edncenos7 c9a1fdca3387 /bin/bash3.在容器内下载php5.4等插件&#xff0c;以便提交成为新镜像 wget --no-ch…