通过Annotation将用户操作记录到数据库表功能实现

一、背景

在用户对我们所开发的系统访问的时候,需要我们的系统具有强大的健壮性,使得给与用户的体验感十足。在业务开发的过程中,我们通过将几个相关的操作绑定成一个事件,使得安全性以及数据的前后一致性得到提高。但是在溯源方面,我们往往没有很好的解决方案,我们无法得知错误的具体信息,这给后期的debug带来了一定的负担,那么我们如果将用户的操作具体信息可以记录到数据库中,那么是不是就可以溯源了?

二、任务:将一个员工管理系统中的增删改相关接口的操作日志记录到数据库中。

三、实现

3.1 我们需要插入一个数据表:log代表着插入数据的格式(数据表中不需要有任何数据):

sql 复制代码
-- 操作日志表
create table operate_log(
    id int unsigned primary key auto_increment comment 'ID',
    operate_user int unsigned comment '操作人ID',
    operate_time datetime comment '操作时间',
    class_name varchar(100) comment '操作的类名',
    method_name varchar(100) comment '操作的方法名',
    method_params varchar(1000) comment '方法参数',
    return_value varchar(2000) comment '返回值',
    cost_time bigint comment '方法执行耗时, 单位:ms'
) comment '操作日志表';

除此之外,我们需要在pom文件中引入fastjson和aop的依赖:

XML 复制代码
<!--        aop依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>

3.2 目录结构如下所示:

3.3 首先我们需要定义一个操作类OperateLog

java 复制代码
package com.bytedance.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperateLog {
    private Integer id; //ID
    private Integer operateUser; //操作人ID
    private LocalDateTime operateTime; //操作时间
    private String className; //操作类名
    private String methodName; //操作方法名
    private String methodParams; //操作方法参数
    private String returnValue; //操作方法返回值
    private Long costTime; //操作耗时
}

3.4 其次我们需要定义一个注解文件Log

java 复制代码
package com.bytedance.anno;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME) // 当前注解什么时候生效
@Target(ElementType.METHOD) // 注解有效的地方

//定义一个注解
public @interface Log {
}

3.5 接着我们定义一个LogAspect的切面,这一部分是将这些代码从主干(controller)中抽离出来,以便于复用和改动,如果我们在每个类或对象中都重复实现这些行为,那么会导致代码的冗余、复杂和难以维护。

java 复制代码
package com.bytedance.aop;
import com.alibaba.fastjson.JSONObject;
import com.bytedance.mapper.OperateLogMapper;
import com.bytedance.pojo.OperateLog;
import com.bytedance.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Array;
import java.time.LocalDateTime;
import java.util.Arrays;

// 定义切面类
@Component
@Aspect // 这是一个切面类
@Slf4j
public class LogAspect {
    // 注入OperateLogMapper的bean对象
    @Autowired
    private OperateLogMapper operateLogMapper;
    @Autowired
    private HttpServletRequest request;
    // 定义一个通知方法
    @Around("@annotation(com.bytedance.anno.Log)") // 匹配的是方法上的加有Log注解的方法
    public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取操作人id - 当前登陆的员工的id 即获得请求头中的jwt令牌,解析
        String jwt = request.getHeader("token");
        Claims claims = JwtUtils.parseJWT(jwt);
        Integer operateUserId = (Integer) claims.get("id");
        // 操作时间
        LocalDateTime operateTime = LocalDateTime.now();
        // 操作类名
        String className = joinPoint.getTarget().getClass().getName();
        // 操作方法名
        String methodName = joinPoint.getSignature().getName();
        // 操作方法参数
//        String methodParams = joinPoint.getArgs().toString();注意不能这么写
        Object[] args = joinPoint.getArgs();
        String methodParams = Arrays.toString(args);
        // 方法开始时间
        long start = System.currentTimeMillis();
        // 调用原始方法运行
        Object res = joinPoint.proceed();
        // 方法返回值
        String returnValue = JSONObject.toJSONString(res);
        // 方法结束时间
        long end = System.currentTimeMillis();
        // 操作耗时
        long costTime = end - start;

        // 记录操作日志 需要调用OperateLogMapper中的insert方法,所以得注入bean对象
        OperateLog operateLog = new OperateLog(null , operateUserId,operateTime,className,methodName,methodParams,returnValue,costTime);
        operateLogMapper.insert(operateLog);
        log.info("AOP记录操作日志:{}",operateLog);
        return res;
    }
}

3.6 OperateLogMapper负责将改动的数据写入数据库中

java 复制代码
package com.bytedance.mapper;

import com.bytedance.pojo.OperateLog;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface OperateLogMapper {

    //插入日志数据
    @Insert("insert into operate_log (operate_user, operate_time, class_name, method_name, method_params, return_value, cost_time) " +
            "values (#{operateUser}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime});")
    public void insert(OperateLog log);

}

3.7 最后将EmpController中需要记录日志的操作上打上@Log的标注即可。

java 复制代码
package com.bytedance.controller;

import com.bytedance.anno.Log;
import com.bytedance.pojo.Emp;
import com.bytedance.pojo.PageBean;
import com.bytedance.pojo.Result;
import com.bytedance.service.DeptService;
import com.bytedance.service.EmpService;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.annotations.Delete;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDate;
import java.util.List;

@RestController
@Slf4j
@RequestMapping("/emps")
public class EmpController {

    @Autowired
    private EmpService empService;

    @GetMapping
    public Result page(@RequestParam(defaultValue = "1") Integer page ,
                       @RequestParam(defaultValue = "10") Integer pageSize,
                       String name, Short gender, @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin , @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end){
        log.info("分页参数为:{},{},{},{},{},{}",page,pageSize,name,gender,begin,end);
        PageBean pageBean = empService.page(page,pageSize,name,gender,begin,end);
        return Result.success(pageBean);
    }

    // 批量删除员工信息
    @Log
    @DeleteMapping("/{ids}")
    public Result delete(@PathVariable List<Integer> ids){
        log.info("批量删除员工参数为:{}",ids);
        empService.delete(ids);
        return Result.success();
    }

    // 添加员工信息
//    @PostMapping
//    public Result add(@RequestBody Emp emp){
//        log.info("新增员工信息为:{emp}",emp);
//        empService.add(emp);
//        return Result.success();
//    }

    // 根据id查询员工信息
    @GetMapping("/{id}")
    public Result getById(@PathVariable Integer id){
        log.info("根据ID查询员工信息:{}",id);
        Emp emp = empService.getById(id);
        return Result.success(emp);
    }


    // 修改员工信息
    @Log
    @PutMapping
    public Result update(@RequestBody Emp emp){
        log.info("前端传过来的员工信息:{}",emp);
        empService.update(emp);
        return Result.success();
    }

}

尝试一下,成功!

注:1、记得将引入的包名换成自己的名字。

2、如果在LogAspect切面中报错,可能你没有jwt的实现类

java 复制代码
package com.bytedance.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.Map;

public class JwtUtils {

    private static String signKey = "itheima";
    private static Long expire = 432000000L; // 240小时候过期

    /**
     * 生成JWT令牌
     * @param claims JWT第二部分负载 payload 中存储的内容
     * @return
     */
    public static String generateJwt(Map<String, Object> claims){
        String jwt = Jwts.builder()
                .addClaims(claims)
                .signWith(SignatureAlgorithm.HS256, signKey)
                .setExpiration(new Date(System.currentTimeMillis() + expire))
                .compact();
        return jwt;
    }

    /**
     * 解析JWT令牌
     * @param jwt JWT令牌
     * @return JWT第二部分负载 payload 中存储的内容
     */
    public static Claims parseJWT(String jwt){
        Claims claims = Jwts.parser()
                .setSigningKey(signKey)
                .parseClaimsJws(jwt)
                .getBody();
        return claims;
    }
}

3、如果这里的params对象是这样的,那可能是切面中获取params的方法写的不对:

java 复制代码
 // 操作方法参数
//        String methodParams = joinPoint.getArgs().toString();注意不能这么写
        Object[] args = joinPoint.getArgs();
        String methodParams = Arrays.toString(args);
相关推荐
isolusion6 分钟前
Springboot的创建方式
java·spring boot·后端
zjw_rp35 分钟前
Spring-AOP
java·后端·spring·spring-aop
Oneforlove_twoforjob1 小时前
【Java基础面试题033】Java泛型的作用是什么?
java·开发语言
TodoCoder1 小时前
【编程思想】CopyOnWrite是如何解决高并发场景中的读写瓶颈?
java·后端·面试
向宇it1 小时前
【从零开始入门unity游戏开发之——C#篇24】C#面向对象继承——万物之父(object)、装箱和拆箱、sealed 密封类
java·开发语言·unity·c#·游戏引擎
小蜗牛慢慢爬行1 小时前
Hibernate、JPA、Spring DATA JPA、Hibernate 代理和架构
java·架构·hibernate
星河梦瑾2 小时前
SpringBoot相关漏洞学习资料
java·经验分享·spring boot·安全
黄名富2 小时前
Redis 附加功能(二)— 自动过期、流水线与事务及Lua脚本
java·数据库·redis·lua
love静思冥想2 小时前
JMeter 使用详解
java·jmeter
言、雲2 小时前
从tryLock()源码来出发,解析Redisson的重试机制和看门狗机制
java·开发语言·数据库