通过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);
相关推荐
极客先躯7 分钟前
中级java每日一道面试题-2024年7月3日
java·开发语言·java每日一道面试题
AskHarries24 分钟前
Spring Boot集成geode快速入门Demo
java·spring boot·后端·geode
小哇66637 分钟前
SpringBoot整合Minio
java·spring boot·spring
Tech Synapse1 小时前
Java循环创建对象内存溢出怎么解决
java·开发语言·jvm
IT·陈寒1 小时前
Kotlin vs Java:深入解析两者之间的最新差异与优劣(全面指南)
java·python·kotlin
行动π技术博客1 小时前
spring中IOC相关介绍
java·spring·rpc
吃青椒的小新1 小时前
独一无二的设计模式——单例模式(Java实现)
java·后端·单例模式·设计模式
天才梦浪1 小时前
开源租房项目
java·项目
杰哥在此1 小时前
Java面试题:解释跨站脚本攻击(XSS)的原理,并讨论如何防范
java·开发语言·面试·编程·xss
Czi橙2 小时前
玩玩快速冥(LeetCode50题与70题以及联系斐波那契)
java·算法·快速幂·斐波那契