SpringBoot系列:通过AOP+注解优雅实现操作日志记录

文章目录

  • 前言
  • 一、简介
    • [1.1 操作日志在企业应用中的重要性](#1.1 操作日志在企业应用中的重要性)
    • [1.2 使用AOP和注解实现操作日志记录的好处](#1.2 使用AOP和注解实现操作日志记录的好处)
  • 二、开发环境
  • 三、准备工作
    • [3.1 创建操作日志记录表](#3.1 创建操作日志记录表)
    • [3.2 创建系统日志实体类](#3.2 创建系统日志实体类)
  • 四、代码实现
    • [4.1 创建业务枚举类](#4.1 创建业务枚举类)
    • [4.2 创建日志注解](#4.2 创建日志注解)
    • [4.3 创建操作状态枚举类](#4.3 创建操作状态枚举类)
    • [4.4 创建IP工具类](#4.4 创建IP工具类)
    • [4.5 创建切面类](#4.5 创建切面类)
    • [4.6 操作日志注解使用](#4.6 操作日志注解使用)
  • 五、测试
  • 六、总结

前言

在企业应用开发中,操作日志记录是确保系统安全性、可追溯性以及调试分析的重要手段之一。通过记录用户的操作行为,不仅可以帮助开发者快速定位问题,还能满足审计和合规需求。本文旨在探讨如何在SpringBoot应用程序中通过AOP(面向切面编程)和自定义注解实现操作日志记录,并将日志存储到数据库中。我们将详细介绍实现这一功能的完整流程,包括项目环境搭建、数据库设计、代码实现及测试验证等步骤。

一、简介

1.1 操作日志在企业应用中的重要性

操作日志在企业应用中扮演着至关重要的角色。它不仅能够记录用户的操作行为,还能帮助开发和运维人员快速定位和解决问题,提升系统的稳定性和安全性。通过记录操作日志,企业可以:

  • 监控用户行为:了解用户在系统中的操作轨迹,分析用户行为,改进用户体验。
  • 故障排查:发生问题时,通过日志快速找到问题的根源,缩短问题排查时间。
  • 审计与合规:记录关键操作,满足法律法规和行业标准的要求,防止恶意操作和数据泄露。
  • 性能分析:分析操作日志,可以发现系统性能瓶颈,指导性能优化。

1.2 使用AOP和注解实现操作日志记录的好处

在SpringBoot项目中,通过AOP(面向切面编程)和自定义注解来实现操作日志记录具有诸多好处:

  • 分离关注点:将日志记录逻辑从业务代码中分离出来,保持代码的清洁和可维护性。
  • 减少重复代码:避免在每个业务方法中手动添加日志记录代码,提升开发效率。
  • 灵活性与可配置性:通过注解配置不同的日志记录需求,灵活应对各种场景。
  • 统一管理与维护:集中管理日志记录逻辑,方便后续的功能扩展和维护。

二、开发环境

  • JDK版本:JDK 17
  • Spring Boot版本:Spring Boot 3.2.2
  • MySQL版本:8.0.37
  • Redis版本:5.0.14.1
  • 构建工具:Maven

三、准备工作

3.1 创建操作日志记录表

java 复制代码
CREATE TABLE `sys_oper_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '日志主键',
  `title` varchar(50) DEFAULT '' COMMENT '模块标题',
  `business_type` varchar(20) DEFAULT '0' COMMENT '业务类型(0其它 1新增 2修改 3删除)',
  `method` varchar(100) DEFAULT '' COMMENT '方法名称',
  `request_method` varchar(10) DEFAULT '' COMMENT '请求方式',
  `oper_name` varchar(50) DEFAULT '' COMMENT '操作人员',
  `oper_url` varchar(255) DEFAULT '' COMMENT '请求URL',
  `oper_ip` varchar(128) DEFAULT '' COMMENT '主机地址',
  `oper_param` varchar(2000) DEFAULT '' COMMENT '请求参数',
  `json_result` varchar(2000) DEFAULT '' COMMENT '返回参数',
  `status` int(1) DEFAULT '0' COMMENT '操作状态(1正常 0异常)',
  `error_msg` varchar(2000) DEFAULT '' COMMENT '错误消息',
  `oper_time` datetime DEFAULT NULL COMMENT '操作时间',
  `execute_time` bigint(20) NOT NULL DEFAULT '0' COMMENT '执行时长(毫秒)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=64 DEFAULT CHARSET=utf8 COMMENT='操作日志记录';

3.2 创建系统日志实体类

java 复制代码
/**
 * 操作日志记录
 *
 * @date 2024/07/14
 */
@Data
@Schema(description = "操作日志记录")
@TableName(value = "sys_oper_log")
public class SysOperLog implements Serializable {

    @TableField(exist = false)
    private static final long serialVersionUID = 1L;

    @TableId(type = IdType.AUTO)
    @Schema(description = "日志主键")
    private Long id;

    @Schema(description = "模块标题")
    private String title;

    @Schema(description = "业务类型(0其它 1新增 2修改 3删除)")
    private String businessType;

    @Schema(description = "方法名称")
    private String method;

    @Schema(description = "请求方式")
    private String requestMethod;

    @Schema(description = "操作类别(0其它 1后台用户 2手机端用户)")
    private String operatorType;

    @Schema(description = "操作人员")
    private String operName;

    @Schema(description = "请求URL")
    private String operUrl;

    @Schema(description = "主机地址")
    private String operIp;

    @Schema(description = "请求参数")
    private String operParam;

    @Schema(description = "返回参数")
    private String jsonResult;

    @Schema(description = "操作状态(1正常 0异常)")
    private Integer status;

    @Schema(description = "错误消息")
    private String errorMsg;

    @Schema(description = "操作时间")
    private Date operTime;

    @Schema(description = "执行时长")
    private long executeTime;

}

四、代码实现

4.1 创建业务枚举类

java 复制代码
/**
 * 业务操作类型
 *
 */
public enum BusinessType {

    /**
     * 其他类型
     */
    OTHER,

    /**
     * 新增
     */
    INSERT,

    /**
     * 修改
     */
    UPDATE,

    /**
     * 删除
     */
    DELETE,

    /**
     * 更新状态
     */
    STATUS,

    /**
     * 授权
     */
    ASSIGN

}

4.2 创建日志注解

java 复制代码
/**
 * 自定义操作日志记录注解
 *
 */
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
    /**
     * 模块名称
     */
    String title() default "";

    /**
     * 业务操作类型
     */
    BusinessType businessType() default BusinessType.OTHER;

    /**
     * 是否保存请求参数
     */
    boolean isSaveRequestData() default true;

    /**
     * 是否保存响应数据
     */
    boolean isSaveResponseData() default true;

    /**
     * 排除指定的请求参数
     */
    public String[] excludeParamNames() default {};
}

4.3 创建操作状态枚举类

java 复制代码
/**
 * 操作状态
 * 
 */
public enum BusinessStatus
{
    /**
     * 成功
     */
    SUCCESS,

    /**
     * 失败
     */
    FAIL,
}

4.4 创建IP工具类

java 复制代码
/**
 * IP工具类
 */
public class IpUtil {

    /**
     * 获取ip
     * @param request 请求
     * @return {@link String }
     */
    public static String getIpAddress(HttpServletRequest request) {
        String ipAddress = null;
        try {
            ipAddress = request.getHeader("x-forwarded-for");
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getRemoteAddr();
                if (ipAddress.equals("127.0.0.1")) {
                    // 根据网卡取本机配置的IP
                    InetAddress inet = null;
                    try {
                        inet = InetAddress.getLocalHost();
                    } catch (UnknownHostException e) {
                        e.printStackTrace();
                    }
                    ipAddress = inet.getHostAddress();
                }
            }
            // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
            if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()
                // = 15
                if (ipAddress.indexOf(",") > 0) {
                    ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
                }
            }
        } catch (Exception e) {
            ipAddress="";
        }
        // ipAddress = this.getRequest().getRemoteAddr();

        return ipAddress;
    }

    /**
     * 获取网关ip
     * @param request 请求
     * @return {@link String }
     */
    public static String getGatwayIpAddress(ServerHttpRequest request) {
        HttpHeaders headers = request.getHeaders();
        String ip = headers.getFirst("x-forwarded-for");
        if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
            // 多次反向代理后会有多个ip值,第一个ip才是真实ip
            if (ip.indexOf(",") != -1) {
                ip = ip.split(",")[0];
            }
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = headers.getFirst("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = headers.getFirst("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = headers.getFirst("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = headers.getFirst("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = headers.getFirst("X-Real-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddress().getAddress().getHostAddress();
        }
        return ip;
    }
}

4.5 创建切面类

注意:这里不同的spring-web依赖版本ServletRequestAttributesgetResponse()返回结果是不同的,我这里使用的spring-web:3.2.2,返回值为jakarta包下面的HttpServletResponse,而一些旧版本的就会返回javax包下的,因此要根据自身版本进行修改。

java 复制代码
import cn.hutool.core.thread.threadlocal.NamedThreadLocal;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.support.spring.PropertyPreFilters;
import com.voyager.annotation.Log;
import com.voyager.domain.entity.SysOperLog;
import com.voyager.domain.enums.BusinessStatus;
import com.voyager.entity.User;
import com.voyager.service.SysOperLogService;
import com.voyager.utils.IpUtil;
import com.voyager.utils.UserHolder;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.ArrayUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import java.util.Collection;
import java.util.Date;
import java.util.Map;

/**
 * 日志切面
 */
@Aspect
@Component
@RequiredArgsConstructor
public class LogAspect {

    /**
     * 定义需要排除在日志记录之外的属性名称数组
     */
    private static final String[] EXCLUDE_PROPERTIES = {"password", "oldPassword", "newPassword", "confirmPassword"};

    private final SysOperLogService sysOperLogService;

    /**
     * 使用ThreadLocal维护一个线程局部变量,用于记录操作的耗时
     */
    private static final ThreadLocal<Long> TIME_THREADLOCAL = new NamedThreadLocal<Long>("Cost Time");


    /**
     * 返回通知
     *
     * @param joinPoint 切点
     */
    @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) {
        //调用处理日志的方法
        handleLog(joinPoint, controllerLog, null, jsonResult);

    }

    /**
     * 异常通知
     *
     * @param joinPoint 切点
     * @param e         异常
     */
    @AfterThrowing(pointcut = "@annotation(controllerLog)", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) {
        handleLog(joinPoint, controllerLog, e, null);
    }

    /**
     * 处理请求前执行,此方法旨在记录方法的开始时间。
     *
     * @param joinPoint     切点
     * @param controllerLog 一个注解对象,表示目标方法上标注的注解。这里用于判断方法是否应该被此切面处理。
     */
    @Before(value = "@annotation(controllerLog)")
    public void boBefore(JoinPoint joinPoint, Log controllerLog) {
        TIME_THREADLOCAL.set(System.currentTimeMillis());
    }

    /**
     * 处理操作日志的逻辑。
     * 当方法执行完毕或发生异常时,此方法用于封装和记录操作日志。
     *
     * @param joinPoint     切点,用于获取目标方法的信息。
     * @param controllerLog 控制器上的日志注解,用于获取方法描述等信息。
     * @param e             异常对象,如果方法执行过程中抛出异常。
     * @param jsonResult    方法返回的对象,用于日志记录,此参数可能为null。
     */
    private void handleLog(JoinPoint joinPoint, Log controllerLog, Exception e, Object jsonResult) {
        try {
            // 获取当前请求的属性,包括HttpServletRequest对象。
            RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
            // 如果请求属性为空,则直接返回,不处理日志。
            if (requestAttributes == null) {
                return;
            }
            // 将请求属性转换为ServletRequestAttributes,以便获取HttpServletRequest对象。
            ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
            // 获取HttpServletRequest对象。
            HttpServletRequest request = servletRequestAttributes.getRequest();

            // 重新获取请求属性,目的是为了后续获取请求方法等信息。
            RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
            ServletRequestAttributes http = (ServletRequestAttributes) attributes;
            // 再次获取HttpServletRequest对象。
            HttpServletRequest httpServletRequest = http.getRequest();

            // 创建SysOperLog对象,用于存储操作日志的信息。
            SysOperLog sysOperLog = new SysOperLog();
            // 默认设置操作状态为正常。
            sysOperLog.setStatus(BusinessStatus.SUCCESS.ordinal());
            // 如果方法执行过程中抛出异常,则将操作状态设置为异常。
            if (e != null) {
                // 设置状态为异常
                sysOperLog.setStatus(BusinessStatus.FAIL.ordinal());
                // 设置异常信息。
                sysOperLog.setErrorMsg(e.getMessage());
            }
            // 获取ip地址
            String ipAddress = IpUtil.getIpAddress(request);
            // 设置ip地址
            sysOperLog.setOperIp(ipAddress);
            // 设置请求地址
            sysOperLog.setOperUrl(request.getRequestURI());
            // 获取当前登录的用户信息。
            User user = UserHolder.getUser();
            // 获取用户名
            String username = UserHolder.getUser().getUserName();
            // 设置操作者名称。
            // 设置操作人员
            sysOperLog.setOperName(username);
            // 获取并设置请求方法,例如GET、POST等。
            sysOperLog.setRequestMethod(request.getMethod());

            // 获取目标对象的类名。
            String className = joinPoint.getTarget().getClass().getName();
            // 获取方法名
            String methodName = joinPoint.getSignature().getName();
            // 设置方法名称
            sysOperLog.setMethod(className + "." + methodName + "()");
            // 获取注解中对方法的描述信息
            getControllerMethodDescription(joinPoint, controllerLog, jsonResult, sysOperLog);
            // 计算执行时长(毫秒)
            long executeTime = System.currentTimeMillis() - TIME_THREADLOCAL.get();
            sysOperLog.setExecuteTime(executeTime);
            // 设置操作时间。
            sysOperLog.setOperTime(new Date());
            // 保存操作日志
            sysOperLogService.save(sysOperLog);
        } catch (Exception ex) {
            // 记录处理日志过程中发生的异常。
            ex.printStackTrace();
        }
    }

    /**
     * 从注解中获取控制器方法的描述信息,并填充到操作日志对象中。
     *
     * @param joinPoint     切点对象,用于获取方法名和参数信息。
     * @param controllerLog 控制器日志注解对象,包含标题、业务类型等配置信息。
     * @param jsonResult    方法的返回结果,用于判断是否需要记录响应数据。
     * @param sysOperLog    系统操作日志对象,此处将从controllerLog中获取的信息填充到该对象中。
     */
    private void getControllerMethodDescription(JoinPoint joinPoint, Log controllerLog, Object jsonResult, SysOperLog sysOperLog) {
        //设置操作模块
        sysOperLog.setTitle(controllerLog.title());
        //设置业务类型
        sysOperLog.setBusinessType(controllerLog.businessType().name());

        // 判断是否需要保存请求数据,如果需要,则调用setRequestValue方法进行处理
        if (controllerLog.isSaveRequestData()) {
            //调用设置请求数据的方法
            setRequestValue(joinPoint, sysOperLog, controllerLog.excludeParamNames());
        }

        // 判断是否需要保存响应数据且返回结果不为空,如果满足条件,则将返回结果转为JSON字符串并保存到操作日志中
        if (controllerLog.isSaveResponseData() && !StringUtils.isEmpty(jsonResult)) {
            //设置响应数据
            sysOperLog.setJsonResult(JSON.toJSONString(jsonResult));
        }
    }


    /**
     * 设置操作日志的请求参数信息。
     *
     * @param joinPoint         切点,用于获取方法参数。
     * @param operLog           操作日志对象,用于设置请求参数信息。
     * @param excludeParamNames 需要排除的参数名数组,这些参数不会被记录在日志中。
     */
    private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog, String[] excludeParamNames) {
        // 获取当前请求的属性
        Map<String, String[]> parameterMap = getParameterMap();
        // 如果参数不为空且不为空集合
        if (parameterMap != null && !parameterMap.isEmpty()) {
            // 将参数转换为JSON字符串,通过excludePropertyPreFilter过滤掉不需要记录的参数
            String params = JSONObject.toJSONString(parameterMap, excludePropertyPreFilter(excludeParamNames));
            // 设置操作日志的请求参数,截取前2000个字符以防止过长
            operLog.setOperParam(org.apache.commons.lang3.StringUtils.substring(params, 0, 2000));
        } else {
            // 如果请求参数为空,尝试从方法参数中获取信息
            Object args = joinPoint.getArgs();
            // 如果方法参数不为空
            if (args != null) {
                // 将方法参数转换为字符串,同样支持排除某些参数名
                String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames);
                // 设置操作日志的请求参数,同样截取前2000个字符
                operLog.setOperParam(org.apache.commons.lang3.StringUtils.substring(params, 0, 2000));
            }
        }
    }

    /**
     * 获取当前HTTP请求的参数
     *
     * @return 一个Map,映射参数名称到参数值数组。这允许处理多值参数。
     */
    private static Map<String, String[]> getParameterMap() {
        // 从Spring的RequestContextHolder中获取当前请求的属性
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        // 将RequestAttributes强制转换为ServletRequestAttributes,以便访问HTTP请求特定的属性
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
        // 从ServletRequestAttributes中获取当前HTTP请求对象
        HttpServletRequest request = (HttpServletRequest) servletRequestAttributes.getRequest();
        // 获取请求的所有参数
        Map<String, String[]> parameterMap = request.getParameterMap();
        return parameterMap;
    }


    /**
     * 忽略敏感属性
     *
     * @param excludeParamNames 需要排除的参数名数组
     * @return {@link PropertyPreFilters.MySimplePropertyPreFilter }
     */
    public PropertyPreFilters.MySimplePropertyPreFilter excludePropertyPreFilter(String[] excludeParamNames) {
        return new PropertyPreFilters().addFilter().addExcludes(ArrayUtils.addAll(EXCLUDE_PROPERTIES, excludeParamNames));
    }

    /**
     * 将对象数组转换为字符串,排除指定的参数名(敏感参数)。
     *
     * @param paramsArray       参数数组,可以包含任意类型的对象。
     * @param excludeParamNames 需要排除的参数名数组,这些参数不会被转换为字符串。
     * @return 返回转换后的参数字符串,各参数间以空格分隔。
     */
    private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames) {
        // 使用StringBuilder来构建最终的参数字符串
        StringBuilder params = new StringBuilder();
        // 检查参数数组是否为空或长度为0,避免不必要的处理
        if (paramsArray != null) {
            // 遍历参数数组中的每个对象
            for (Object o : paramsArray) {
                // 检查对象是否为空且不属于被过滤的类型
                if (o != null && !isFilterObject(o)) {
                    try {
                        // 将对象转换为JSON字符串,排除指定的属性
                        Object jsonObj = JSONObject.toJSONString(o, excludePropertyPreFilter(excludeParamNames));
                        // 将转换后的JSON字符串追加到参数字符串中,并以空格分隔各个参数
                        params.append(jsonObj).append(" ");
                    } catch (Exception ignored) {
                        // 忽略转换过程中的异常,确保方法的健壮性
                    }
                }
            }
        }
        return params.toString().trim();
    }


    /**
     * 判断传入的对象是否需要被过滤。
     * 这个方法主要用于处理上传文件时,判断接收的参数是否为文件类型或其他特定类型。
     *
     * @param o 待检查的对象
     * @return 如果对象需要被过滤(即对象为MultipartFile或其他特定类型),则返回true;否则返回false。
     */
    @SuppressWarnings("rawtypes")
    public boolean isFilterObject(final Object o) {
        // 获取对象的类类型
        Class<?> clazz = o.getClass();

        // 检查对象是否为数组类型
        if (clazz.isArray()) {
            // 如果数组的组件类型可以被MultipartFile类转换,则返回true
            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
        } else if (Collection.class.isAssignableFrom(clazz)) {
            // 如果对象是集合类型,将其转换为Collection接口实例
            Collection collection = (Collection) o;
            // 遍历集合中的每个元素,如果任意元素是MultipartFile实例,则返回true
            for (Object value : collection) {
                return value instanceof MultipartFile;
            }
        } else if (Map.class.isAssignableFrom(clazz)) {
            // 如果对象是Map类型,将其转换为Map接口实例
            Map map = (Map) o;
            // 遍历Map中的每个条目,如果任意条目的值是MultipartFile实例,则返回true
            for (Object value : map.entrySet()) {
                Map.Entry entry = (Map.Entry) value;
                return entry.getValue() instanceof MultipartFile;
            }
        }
        // 如果对象不是数组、集合或Map类型,检查它是否为MultipartFile、HttpServletRequest、HttpServletResponse或BindingResult实例
        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
                || o instanceof BindingResult;
    }

}

执行流程分析:

  1. 请求到达 :当一个请求到达目标方法时,切面会首先执行boBefore方法,记录方法的开始时间。这个时间被存储在一个 ThreadLocal 对象中,用于后续计算方法的执行时长。

    java 复制代码
    @Before(value = "@annotation(controllerLog)")
    public void boBefore(JoinPoint joinPoint, Log controllerLog) {
        TIME_THREADLOCAL.set(System.currentTimeMillis());
    }
  2. 方法执行

    • 正常返回 :如果目标方法执行成功并返回结果,切面会执行doAfterReturning方法。这个方法会调用handleLog方法来处理操作日志。
    java 复制代码
    @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) {
        handleLog(joinPoint, controllerLog, null, jsonResult);
    }
    • 异常返回 :如果目标方法执行过程中抛出异常,切面会执行doAfterThrowing方法。这个方法也会调用handleLog方法来处理操作日志,并记录异常信息。
    java 复制代码
    @AfterThrowing(pointcut = "@annotation(controllerLog)", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) {
        handleLog(joinPoint, controllerLog, e, null);
    }
  3. 日志处理 :在handleLog方法中,切面会收集各种请求信息、方法信息、执行时长等数据,并将这些数据封装到一个SysOperLog对象中,最后通过sysOperLogService保存该日志对象。

  4. 获取和设置日志信息 :在handleLog方法内部,通过调用一些辅助方法来获取和设置日志的详细信息,包括请求参数、响应数据等。

4.6 操作日志注解使用

java 复制代码
    /**
     * 获取用户信息
     *
     * @param id 用户id
     * @return {@link Result }<{@link UserInfo }>
     */
    @Log(title = "获取用户信息", businessType = BusinessType.OTHER)
    @Operation(description = "获取用户信息")
    @GetMapping("/{id}")
    public Result<UserInfo> getUser(@PathVariable Long id) {
        return Result.success(userInfoService.getById(id));
    }


    /**
     * 插入用户信息
     *
     * @param userInfo 用户信息
     * @return {@link Result }<{@link String }>
     */
    @Log(title = "插入用户信息", businessType = BusinessType.INSERT)
    @Operation(description = "插入用户信息")
    @PostMapping
    public Result<String> insertUser(@RequestBody UserInfo userInfo) {
        boolean saved = userInfoService.save(userInfo);
        if (!saved) {
            return Result.error("插入失败");
        }
        return Result.success();
    }

    /**
     * 更新用户信息
     *
     * @param userInfo 用户信息
     * @return {@link Result }<{@link String }>
     */
    @Log(title = "更新用户信息", businessType = BusinessType.UPDATE)
    @Operation(description = "更新用户信息")
    @PutMapping
    public Result<String> updateUser(@RequestBody UserInfo userInfo) {
        boolean updated = userInfoService.updateById(userInfo);
        if (!updated) {
            return Result.error("更新失败");
        }
        return Result.success();
    }

    /**
     * 删除用户信息
     * @param id i用户id
     * @return {@link Result }<{@link String }>
     */
    @Log(title = "删除用户信息", businessType = BusinessType.DELETE)
    @Operation(description = "删除用户信息")
    @DeleteMapping("/{id}")
    public Result<String> deleteUser(@PathVariable Long id) {
        boolean deleted = userInfoService.removeById(id);
        if (!deleted) {
            return Result.error("删除失败");
        }
        return Result.success();
    }

五、测试

  1. 分别执行请求四个接口:
  1. 查看数据库

六、总结

本文主要参考了若依框架的操作日志记录功能的实现,记录了操作日志记录功能的实现和其中遇到的一些问题(比如:getResponse()返回值的问题)。在文章的开始,我们探讨了在SpringBoot应用程序中实现日志操作日志记录的重要性,随后采用基于AOP+注解的解决方案,以将日志数据存储到数据库中。通过这个方案,我们能够有效地记录用户的操作行为,从而方便后续的审计和分析,希望对大家有所帮助😊。


附录:

若依仓库地址

相关推荐
木易 士心几秒前
MyBatis Plus 核心功能与用法
java·后端·mybatis
Victor3561 分钟前
MongoDB(93)如何使用变更流跟踪数据变化?
后端
用户6757049885021 分钟前
全网都在推 Claude Code,但只有这篇文章教你如何“真正”能用
后端·aigc·claude
Victor3565 分钟前
MongoDB(94)什么是MongoDB Atlas?
后端
下次再写5 分钟前
互联网大厂Java面试三轮技术问答及详解——涵盖Spring Boot与微服务
java·springboot·microservices·interview
林深时见鹿v8 分钟前
《后端开发全栈工具安装踩坑指南 & 经验沉淀手册》
java·人工智能·python·oracle
Xiu Yan13 分钟前
Java 转 C++ 系列:STL容器之list
java·开发语言·数据结构·c++·stl·list·visual studio
搬砖小郭13 分钟前
从0到1的springboot项目搭建到打包
java
苏三说技术15 分钟前
为什么越来越多的大厂抛弃MCP,转向CLI?
后端
小芝麻咿呀17 分钟前
边缘计算网关-EG8200Mini导轨版
java·人工智能·边缘计算