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

news2024/9/20 14:24:16

文章目录

  • 前言
  • 一、简介
    • 1.1 操作日志在企业应用中的重要性
    • 1.2 使用AOP和注解实现操作日志记录的好处
  • 二、开发环境
  • 三、准备工作
    • 3.1 创建操作日志记录表
    • 3.2 创建系统日志实体类
  • 四、代码实现
    • 4.1 创建业务枚举类
    • 4.2 创建日志注解
    • 4.3 创建操作状态枚举类
    • 4.4 创建IP工具类
    • 4.5 创建切面类
    • 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 创建操作日志记录表

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 创建系统日志实体类

/**
 * 操作日志记录
 *
 * @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 创建业务枚举类

/**
 * 业务操作类型
 *
 */
public enum BusinessType {

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

    /**
     * 新增
     */
    INSERT,

    /**
     * 修改
     */
    UPDATE,

    /**
     * 删除
     */
    DELETE,

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

    /**
     * 授权
     */
    ASSIGN

}

4.2 创建日志注解

/**
 * 自定义操作日志记录注解
 *
 */
@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 创建操作状态枚举类

/**
 * 操作状态
 * 
 */
public enum BusinessStatus
{
    /**
     * 成功
     */
    SUCCESS,

    /**
     * 失败
     */
    FAIL,
}

4.4 创建IP工具类

/**
 * 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包下的,因此要根据自身版本进行修改。

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 对象中,用于后续计算方法的执行时长。

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

    • 正常返回:如果目标方法执行成功并返回结果,切面会执行doAfterReturning方法。这个方法会调用handleLog方法来处理操作日志。
    @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) {
        handleLog(joinPoint, controllerLog, null, jsonResult);
    }
    
    • 异常返回:如果目标方法执行过程中抛出异常,切面会执行doAfterThrowing方法。这个方法也会调用handleLog方法来处理操作日志,并记录异常信息。
    @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 操作日志注解使用

    /**
     * 获取用户信息
     *
     * @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. 分别执行请求四个接口:

image-20240715215935100

image-20240715220040786

image-20240715220105068

image-20240715220002000

  1. 查看数据库

image-20240715220310054

六、总结

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


附录:

若依仓库地址

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux驱动开发-04LED灯驱动实验(直接操作寄存器)

一、Linux 下LED 灯驱动原理 Linux 下的任何外设驱动&#xff0c;最终都是要配置相应的硬件寄存器。驱动访问底层的硬件除了使用内存映射将物理地址空间转化为虚拟地址空间&#xff0c;去进行读写修改&#xff0c;还可以通过各种子系统函数去进行操作 1.1 地址映射 MMU 全称…

WebSocket实现群聊功能、房间隔离

引用WebSocket相关依赖 <dependency><groupId>javax.websocket</groupId><artifactId>javax.websocket-api</artifactId><version>1.1</version></dependency><dependency><groupId>org.springframework</grou…

Golang | Leetcode Golang题解之第235题二叉搜索树的最近公共祖先

题目&#xff1a; 题解&#xff1a; func lowestCommonAncestor(root, p, q *TreeNode) (ancestor *TreeNode) {ancestor rootfor {if p.Val < ancestor.Val && q.Val < ancestor.Val {ancestor ancestor.Left} else if p.Val > ancestor.Val && q…

【区块链 + 智慧政务】基于区块链的可信数据档案管理系统 | FISCO BCOS应用案例

目前&#xff0c;我国的档案管理整体上实行“电子化”和“纸质”同步并存的“双套制”管理体系&#xff0c;这套管理规范体系在一 定程度上满足了电子文件安全存储的要求。但是随着云计算、大数据、区块链等现代信息技术的快速发展&#xff0c;以及 全国“互联网 政务服务”工…

TG创建小程序交互APP登录以及机器人信息

1、搜索 BotFather &#xff0c;输入命令 /newbot 创建机器人。 2、修改机器人信息 /mybots 编辑名称 : 修改机器人名称 编辑关于: 修改关于 hayden yyds&#xff0c;修改以后打开机器人会出现在下图 编辑描述 : 机器人的描述 编辑描述图片 : 机器人的图片 编辑 Botpic…

让前端和后端要“动手”的7大行为,你中了几个。

前后端分离导致了二者在工作中交叉非常多&#xff0c;不像之前前端搞完静态页面&#xff0c;拽给后端就行了。 这种交叉必然导致巨大的沟通成本&#xff0c;贝格前端工场将项目踩过的坑&#xff0c;给大家分享一下。 前端工程师最讨厌的后端行为包括&#xff1a; 1. 不提供清…

高端的食材,往往只需要最简单的烹饪!ORB,仅此一招,Alpha达到年化36%

常常看到有人提问&#xff0c;如何挖掘因子和策略&#xff1f;ORB 策略的改进历史能给我们许多启发。一是一个策略值得研究数十年&#xff1b;二是温故而知新是永远的法宝。沉下心来&#xff0c;真正吃透 IT 系统、吃透数据和已有策略&#xff0c;比追风要好得多。 炒股要炒大…

备考美国数学竞赛AMC10:吃透1200道历年真题和知识点(持续)

距离2024年AMC10美国数学竞赛开赛预计还有3个多月的时间&#xff0c;实践证明&#xff0c;做真题&#xff0c;吃透真题和背后的知识点是备考AMC8、AMC10有效的方法之一。 通过做真题&#xff0c;可以帮助孩子找到真实竞赛的感觉&#xff0c;而且更加贴近比赛的内容&#xff0c…

[AHK] WinHttpRequest.5.1报错 0x80092004 找不到对象或属性

目录 背景描述 用浏览器访问&#xff0c;正常返回 ​编辑 AHK v2官方示例源代码 AHK v2运行结果报错(0x80092004) 找不到对象或属性 用thqby大佬的WinHttpRequest.ahk库测试报错 0x80092004 找不到对象或属性 附&#xff1a; 用Apifox访问&#xff0c;也正常返回 AHK v1 …

怎样优化 PostgreSQL 中对复杂查询的并行执行计划?

&#x1f345;关注博主&#x1f397;️ 带你畅游技术世界&#xff0c;不错过每一次成长机会&#xff01;&#x1f4da;领书&#xff1a;PostgreSQL 入门到精通.pdf 文章目录 怎样优化 PostgreSQL 中对复杂查询的并行执行计划一、了解并行执行计划的基础知识二、优化并行执行计划…

MySQL索引特性(上)

目录 索引的重要 案例 认识磁盘 MySQL与存储 先来研究一下磁盘 扇区 定位扇区 结论 磁盘随机访问与连续访问 MySQL与磁盘交互基本单位 建立共识 索引的理解 建立测试表 插入多条记录 局部性原理 所有的MySQL的操作(增删查改)全部都是在MySQL当中的内存中进行的&am…

基于AT89C51单片机GSM模块的家庭防火防盗报警系统设计(含文档、源码与proteus仿真,以及系统详细介绍)

本篇文章论述的是基于AT89C51单片机GSM模块的家庭防火防盗报警系统设计的详情介绍&#xff0c;如果对您有帮助的话&#xff0c;还请关注一下哦&#xff0c;如果有资源方面的需要可以联系我。 目录 摘 要 原理图 仿真图 元器件清单 代码 系统论文 参考文献 资源下载…

内衣专用洗衣机怎么样?五样超卓臻品专业推荐!

在日常生活中&#xff0c;内衣洗衣机已成为现代家庭必备的重要家电之一。选择一款耐用、质量优秀的内衣洗衣机&#xff0c;不仅可以减少洗衣负担&#xff0c;还能提供高效的洗涤效果。然而&#xff0c;市场上众多内衣洗衣机品牌琳琅满目&#xff0c;让我们往往难以选择。那么&a…

【Python】Selenium怎么切换浏览器的页面

我们在爬网使用Selenium进行测试的时候&#xff0c;有时候想要点击浏览器里面的网址&#xff0c;跳到另一个页面上&#xff0c;获取第二个页面的内容。 可是有时候从官网进去&#xff0c;点击跳转到下一个页面以后&#xff0c;却没法定位到下一个页面的元素&#xff0c;这时候就…

【JVM基础01】——介绍-初识JVM运行流程

目录 1- 引言&#xff1a;初识JVM1-1 JVM是什么&#xff1f;(What)1-1-1 概念1-1-2 优点 1-2 为什么学习JVM?(Why) 2- 核心&#xff1a;JVM工作的原理&#xff08;How&#xff09;⭐2-1 JVM 的组成部分及工作流程2-2 学习侧重点 3- 小结(知识点大纲)&#xff1a;3-1 JVM 组成3…

Ubuntu 22.04.4 LTS (linux) 安装certbot 免费ssl证书申请 letsencrypt

1 安装certbot sudo apt update sudo apt-get install certbot 2 申请letsencrypt证书 sudo certbot certonly --webroot -w 网站目录 -d daloradius.域名.com 3 修改nginx 配置ssl 证书 # 配置服务器证书 ssl_certificate /etc/letsencrypt/live/daloradius.域名.com/f…

FPGA:基于复旦微FMQL10S400 /FMQL20S400 国产化核心板

复旦微电子是国内集成电路设计行业的领军企业之一&#xff0c;早在2000年就在香港创业板上市&#xff0c;成为行业内首家上市公司。公司的RFID芯片、智能卡芯片、EEPROM、智能电表MCU等多种产品在市场上的占有率位居行业前列。 今天介绍的是搭载复旦微 FMQL10S400/FMQL20S400的…

16001.WSL2 ubuntu20.04 编译安装 vsomeip

文章目录 1 vsomeip 编译安装1.1 vsomeip的安装1.2 编译提示错误1.3 编译hello_world示例1.4 运行服务器端 1 vsomeip 编译安装 1.1 vsomeip的安装 参考博文 https://blog.csdn.net/peterwanye/article/details/128386539 1.2 编译提示错误 ubuntu1-BJ-EE1000042:~/opt/vso…

【持续集成_05课_Linux部署SonarQube及结合开发项目部署】

一、Linux下安装SonarQube 1、安装sonarQube 前置条件&#xff1a;sonarQube不能使用root账号进行启动&#xff0c;所以需要创建普通用户及 其用户组 1&#xff09;创建组 2&#xff09;添加用户、组名、密码 3&#xff09;CMD上传qube文件-不能传到home路径下哦 4&#xff09…

【NLP大模型】词嵌入的空间表示与应用

文章目录 一、语义特征空间二、引入新维度&#xff1a;皇室三、语义特征向量的用途四、向量运算类比五、词嵌入的维度和应用词嵌入的应用 六、测量欧几里得距离向量计算向量和欧几里得距离 七、使用点积测量相似度八、创建词嵌入 一、语义特征空间 考虑“男人”、“女人”、“…