SpringBoot 项目实现操作日志的记录(使用 AOP 注解模式)

发布于:2025-05-21 ⋅ 阅读:(16) ⋅ 点赞:(0)

本文是博主在做关于如何记录用户操作日志时做的记录,常见的项目中难免存在一些需要记录重要日志的部分,例如权限和角色设定,重要数据的操作等部分。

博主使用 Spring 中的 AOP 功能,结合注解的方式,对用户操作过的一些重要方法做日志记录,存储到数据库中,可以随时查阅。

本文使用技术:SpringBoot 3 + JDK17 + MyBatis-Plus + MySQL。


01、PostMan 测试结果

青灯文案-测试

02、数据库存储数据后展示样式

在这里插入图片描述

03、数据库表创建语句

CREATE TABLE project_log (
  id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '日志ID',
  company_id varchar(36) DEFAULT NULL COMMENT '公司ID',
  company varchar(200) DEFAULT NULL COMMENT '公司',
  uname varchar(100) DEFAULT NULL COMMENT '用户名',
  uid bigint(20) DEFAULT NULL COMMENT '用户ID',
  operate_type int(10) DEFAULT NULL COMMENT '操作类型',
  operate_module varchar(64) DEFAULT NULL COMMENT '操作模块',
  operate_desc varchar(255) DEFAULT NULL COMMENT '操作描述',
  operate_param text DEFAULT NULL COMMENT '操作参数',
  operate_time bigint(20) DEFAULT NULL COMMENT '操作时间',
  operate_result tinyint(10) DEFAULT NULL COMMENT '操作结果',
  PRIMARY KEY (id)
);

create index idx_operate_type
    on project_log (operate_type);

create index idx_operate_module
    on project_log (operate_module);

create index idx_uid
    on project_log (uid);

create index idx_company
    on project_log (company);

create index idx_uname
    on project_log (uname);

04、项目结构图

青灯文案-项目结构

05、代码:application.yml

server:
  port: 8089

spring:
  datasource:
    url: jdbc:mysql://10.100.4.163:3306/test?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456

06、代码:pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
         
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.wen</groupId>
    <artifactId>Test-Operate-Log</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-jsqlparser</artifactId>
            <version>3.5.9</version>
        </dependency>

        <!--mybatis-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>3.5.9</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-core</artifactId>
            <version>1.3.5</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>

        <dependency>
            <groupId>com.fhs-opensource</groupId>
            <artifactId>easy-trans-mybatis-plus-extend</artifactId>
            <version>3.0.6</version>
        </dependency>

        <dependency>
            <groupId>com.oracle.database.jdbc</groupId>
            <artifactId>ojdbc8</artifactId>
        </dependency>

        <dependency>
            <groupId>org.mybatis.dynamic-sql</groupId>
            <artifactId>mybatis-dynamic-sql</artifactId>
            <version>1.4.0</version>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.1</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.83</version>
        </dependency>
        
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <configuration>
                    <archive>
                        <!--生成的jar 不要包含pom.xml pom.properties 这2个文件-->
                        <addMavenDescriptor>false</addMavenDescriptor>
                        <manifest>
                            <!--是否要不第三番jar放到manifest的classpath中-->
                            <addClasspath>true</addClasspath>
                            <!--生成的manifest中的classpath的前缀,因为要把第三方jar放到lib目录下,所以classpath前缀是lib/-->
                            <classpathPrefix>lib/</classpathPrefix>
                            <!--应用的main class -->
                            <mainClass>cn.com.wind.server.Application</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

07、代码:注解 LogRecord

package com.wen.common;

import com.wen.common.OperateTypeEnum;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author : rjw
 * @date : 2025-05-19
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogRecord {
    OperateTypeEnum operateType() default OperateTypeEnum.OTHER;
}

08、代码:LogRecordAop

package com.wen.common;

import cn.hutool.core.util.ArrayUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.wen.mapper.LogRecordMapper;
import com.wen.model.UserInfo;
import com.wen.model.ProjectLog;
import com.wen.service.IUserService;
import com.wen.utils.LogAopUtils;
import com.wen.utils.LogHolder;
import com.wen.utils.UserHolder;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.util.Arrays;

/**
 * @author : rjw
 * @date : 2025-05-07
 */
@Aspect
@Component
@Slf4j
public class LogRecordAop {

    @Autowired
    private IUserService userService;
    
    @Autowired
    private LogRecordMapper logRecordMapper;

    /**
     * 切入点 使用注解路径
     */
    @Pointcut("@annotation(com.wen.common.LogRecord)")
    public void record() {
    }

	/**
     * 前置操作
     * @param point 切入点
     */
    @Before("record() && @annotation(logRecord)")
    public void before(JoinPoint point, LogRecord logRecord) {
        ProjectLog logInfo = new ProjectLog();
        UserInfo user = UserHolder.getCurrentUser();
        // id 自增
        if (user != null) {
            logInfo.setUid(user.getUid());
            logInfo.setUname(user.getName());
            logInfo.setCompanyId(user.getCompanyId() == null ? "" : user.getCompanyId());
            logInfo.setCompany(user.getCompany());
        } else {
            // 如果为null,则说明开发人员使用接口调用
            logInfo.setUid(-1L);
            logInfo.setUname("开发人员");
            logInfo.setCompanyId("");
            logInfo.setCompany("tx");
        }
        logInfo.setOperateType(logRecord.operateType().getCode());
        logInfo.setOperateModule(logRecord.operateType().getModule());
        logInfo.setOperateTime(System.currentTimeMillis());
        Object[] args = LogAopUtils.removeUnnecessaryArgs(point.getArgs());
        logInfo.setOperateDesc(genOperateDesc(logRecord.operateType(), args, logInfo));
        if (ArrayUtil.isNotEmpty(args)) {
            if (logRecord.operateType() == OperateTypeEnum.EXPORT) {
                logInfo.setOperateParam(JSON.toJSONString(args[0]));
            } else {
                logInfo.setOperateParam(JSON.toJSONString(args));
            }
        }
        LogHolder.setProjectLog(logInfo);
    }

    private String genOperateDesc(OperateTypeEnum operateType, Object[] args, ProjectLog projectLog) {
        String operateDesc = String.format("【%s】-%s", operateType.getModule(), operateType.getFunction());
        if (ArrayUtil.isEmpty(args)) {
            return operateDesc;
        }
        switch (operateType) {
            case OPERATE:
                operateDesc = String.format(operateDesc, Arrays.toString(args));
                break;
            default:
                JSONObject req = JSONObject.parseObject(JSON.toJSONString(args[0]));
                operateDesc = String.format(operateDesc, req.toJSONString());
                break;
        }
        log.info("operateDesc: {}", operateDesc);
        return operateDesc;
    }

    @AfterReturning(value = "record()", returning = "response")
    public void afterReturning(Object response) {
        try {
            ProjectLog logInfo = LogHolder.getProjectLog();
            //   ======== 这里要求要使用返回值   
            if (logInfo == null || response == null) {
                return;
            }
            logInfo.setOperateResult((byte) 400);
            if (!checkParam(logInfo)) {
                log.warn("add log to DB failed cause param error! complianceLog:{}", logInfo);
                return;
            }
            addPrivateQuoteLog(logInfo);
        } finally {
            LogHolder.removeProjectLog();
        }
    }

	@AfterThrowing(value = "record()", throwing = "t")
    public void afterThrowing(Throwable t) {
        try {
            ProjectLog infoLog = LogHolder.getProjectLog();
            if (infoLog == null) {
                return;
            }
            infoLog.setOperateResult((byte) 400);
            if (!checkParam(infoLog)) {
                log.warn("add log to DB failed cause param error! complianceLog:{}", infoLog);
                return;
            }
            addPrivateQuoteLog(infoLog);
        } finally {
            LogHolder.getProjectLog();
        }
    }

    @Async
    private void addPrivateQuoteLog(ProjectLog projectLog) {
        logRecordMapper.insert(projectLog);
    }

    private boolean checkParam(ProjectLog projectLog) {
        return projectLog != null &&
                projectLog.getCompanyId() != null &&
                projectLog.getCompany() != null &&
                projectLog.getUid() != null &&
                projectLog.getOperateResult() != null &&
                projectLog.getOperateTime() != null &&
                projectLog.getOperateType() != null &&
                projectLog.getUname() != null;
    }
}

09、代码:OperateTypeEnum

package com.wen.common;

import lombok.Getter;

/**
 * @author : rjw
 * @date : 2025-05-19
 */
@Getter
public enum OperateTypeEnum {
    OTHER(0, "系统","其它类型"),
    OPERATE(1, "内部接口—操作", "操作"),
    EXPORT(2, "内部接口—导出", "导出"),
    MAX(100, "", "")
    ;

    private final int code;
    private final String module;
    private final String function;

    OperateTypeEnum(int code, String module, String function) {
        this.code = code;
        this.module = module;
        this.function = function;
    }
}

10、代码:TestController

package com.wen.controller;

import com.wen.common.LogRecord;
import com.wen.common.OperateTypeEnum;
import com.wen.model.UserInfo;
import com.wen.service.IUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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;

/**
 * @author : rjw
 * @date : 2025-05-19
 */
@Slf4j
@RestController
@RequestMapping("/system")
public class TestController {

    @Autowired
    private IUserService userService;
    
    @GetMapping("/testLog")
    @LogRecord(operateType = OperateTypeEnum.OPERATE)
    public UserInfo updateQuoteChannelPush(@RequestParam("id") long id, @RequestParam("name") String name,
                @RequestParam("company") String company, @RequestParam("companyId") String companyId) {
        userService.getCurrentUser();
        // 和内部数据无关
        UserInfo userInfo = new UserInfo();
        userInfo.setUid(id);
        userInfo.setName(name);
        userInfo.setCompany(company);
        userInfo.setCompanyId(companyId);
        log.info("userInfo is {}", userInfo);
        // 要有返回值,不然不能记录,可以在AOP中自己调
        return userInfo;
    }
}

11、代码:LogRecordMapper(使用 MyBatis-Plus)

package com.wen.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.wen.model.ProjectLog;

/**
 * @author : rjw
 * @date : 2025-05-19
 */
public interface LogRecordMapper extends BaseMapper<ProjectLog> {

}

12、代码:ProjectLog(实体类)

package com.wen.model;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

/**
 * @author : rjw
 * @date : 2025-05-19
 */
@Data
@TableName("project_log")
public class ProjectLog {

    /**
     * 日志 ID
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long logId;

    /**
     * 公司ID
     */
    private String companyId;

    /**
     * 公司
     */
    private String company;

    /**
     * 用户名
     */
    private String uname;

    /**
     * 用户ID
     */
    private Long uid;

    /**
     * 操作类型
     */
    private Integer operateType;

    /**
     * 操作模块
     */
    private String operateModule;

    /**
     * 操作描述
     */
    private String operateDesc;

    /**
     * 操作参数
     */
    private String operateParam;

    /**
     * 操作时间
     */
    private Long operateTime;

    /**
     * 操作结果
     */
    private Byte operateResult;
}

13、代码:UserInfo

package com.wen.model;

import lombok.Data;

/**
 * @author : rjw
 * @date : 2025-05-19
 */
@Data
public class UserInfo {

    private long uid;

    private String name;

    private String company;
    
    private String companyId;
}

14、代码:UserServiceImpl

package com.wen.service.impl;

import com.wen.model.UserInfo;
import com.wen.service.IUserService;
import com.wen.utils.UserHolder;
import org.springframework.stereotype.Service;

/**
 * @author : rjw
 * @date : 2025-05-19
 */
@Service
public class UserServiceImpl implements IUserService {

    @Override
    public UserInfo getCurrentUser() {
        /**
         * 一般是登陆的时候设置当前用户
         * 下线的时候删除当前用户
         */
        UserInfo userInfo = new UserInfo();
        userInfo.setUid(1000001);
        userInfo.setName("张三");
        userInfo.setCompany("tx");
        userInfo.setCompanyId("tx");
        UserHolder.setCurrentUser(userInfo);
        return userInfo;
    }
}

15、代码:IUserService

package com.wen.service;

import com.wen.model.UserInfo;

/**
 * @author : rjw
 * @date : 2025-05-19
 */
public interface IUserService {

    UserInfo getCurrentUser();

}

16、代码:LogAopUtils

package com.wen.utils;

import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.catalina.connector.RequestFacade;
import org.apache.catalina.connector.ResponseFacade;
import org.springframework.web.multipart.MultipartFile;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class LogAopUtils {
    public final static Set<Class<?>> UNNECESSARY_CLASS = new HashSet<Class<?>>(){
        {
            add(ServletRequest.class);
            add(ServletResponse.class);
            add(HttpServletRequest.class);
            add(HttpServletResponse.class);
            add(RequestFacade.class);
            add(ResponseFacade.class);
        }
    };

    public final static Set<Class<?>> UNNECESSARY_SUPER_CLASSES = new HashSet<Class<?>>() {
        {
            add(MultipartFile.class);
        }
    };

    public static Object[] removeUnnecessaryArgs(Object[] args) {
        List<Object> list = new ArrayList<>();
        outer: for (Object arg : args) {
            if (UNNECESSARY_CLASS.contains(arg.getClass())) {
                continue;
            }
            for (Class<?> superClass : UNNECESSARY_SUPER_CLASSES) {
                if(superClass.isAssignableFrom(arg.getClass())) {
                    continue outer;
                }
            }
            list.add(arg);
        }
        return list.toArray();
    }
}

17、代码:LogHolder

package com.wen.utils;

import com.wen.model.ProjectLog;

public class LogHolder {

    private static final ThreadLocal<ProjectLog> LOCAL_PROJECT_LOG = new ThreadLocal<>();

    public static ProjectLog getProjectLog() {
        return LOCAL_PROJECT_LOG.get();
    }

    public static void setProjectLog(ProjectLog user) {
        LOCAL_PROJECT_LOG.set(user);
    }

    public static void removeProjectLog() {
        LOCAL_PROJECT_LOG.remove();
    }
}

18、代码:UserHolder

package com.wen.utils;

import com.wen.model.UserInfo;

/**
 * @author : rjw
 * @date : 2025-05-07
 */
public class UserHolder {

    private static final ThreadLocal<UserInfo> CURRENT_USER = new ThreadLocal<>();

    public static UserInfo getCurrentUser() {
        return CURRENT_USER.get();
    }
    
    public static void setCurrentUser(UserInfo user) {
        CURRENT_USER.set(user);
    }
    
    public static void removeCurrentUser() {
        CURRENT_USER.remove();
    }
}

19、代码:Application

package com.wen;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author : rjw
 * @date : 2025-05-19
 */
@SpringBootApplication
@MapperScan(basePackages = {"com.wen.mapper"})
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }
}

20、代码:LogRecordMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wen.mapper.LogRecordMapper">


</mapper>


网站公告

今日签到

点亮在社区的每一天
去签到