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

文章目录

  • 前言
  • 一、简介
    • 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")) {// 根据网卡取本机配置的IPInetAddress inet = null;try {inet = InetAddress.getLocalHost();} catch (UnknownHostException e) {e.printStackTrace();}ipAddress = inet.getHostAddress();}}// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()// = 15if (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才是真实ipif (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类转换,则返回truereturn clazz.getComponentType().isAssignableFrom(MultipartFile.class);} else if (Collection.class.isAssignableFrom(clazz)) {// 如果对象是集合类型,将其转换为Collection接口实例Collection collection = (Collection) o;// 遍历集合中的每个元素,如果任意元素是MultipartFile实例,则返回truefor (Object value : collection) {return value instanceof MultipartFile;}} else if (Map.class.isAssignableFrom(clazz)) {// 如果对象是Map类型,将其转换为Map接口实例Map map = (Map) o;// 遍历Map中的每个条目,如果任意条目的值是MultipartFile实例,则返回truefor (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 = "插入用户信息")@PostMappingpublic 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 = "更新用户信息")@PutMappingpublic 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.rhkb.cn/news/377743.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[A-04] ARMv8/ARMv9-Cache的相关策略

ver0.3 前言 前面我们已经通过三篇文章反反复复的讲Cache的概念、结构、架构&#xff0c;相信大家对Cache已经大概有了初步的了解。这里简单归纳一下: (1) Cache从硬件视角看&#xff0c;是连接PE-Core和主存的一种存储介质&#xff0c;存储的数据是主存中数据的副本&#xf…

【算法消费者行为】算法性能预期如何增强冲动性购买?—推文分享—2024-07-16

今天的推文主题是&#xff1a;【算法&消费者行为】 第一篇&#xff1a;如何通过管理多种刺激来优化客户体验&#xff1f;购买行为的驱动因素是隐性还是显性的消费者态度&#xff1f;第二篇&#xff1a;算法性能期望如何增强在线零售中的即兴购买行为。第三篇&#xff1a;社…

【Linux】Ubuntu 漏洞扫描与修复的吃瘪经历

自从上次“劫持”事情后&#xff0c;项目经理将所有跟安全相关的都推给我了&#xff08;不算 KPI 又要被白嫖&#xff0c;烦死了&#xff09;。这次客户又提了一个服务器安全扫描和漏洞修复的“活”&#xff0c;我这边顺手将过程记录一下&#xff0c;就当经验总结跟各位分享一下…

Parallels Desktop 19 for Mac(PD19虚拟机)详细图文安装教程分享

Parallels Desktop 19是一款功能丰富、性能强大且易于使用的虚拟机软件&#xff0c;它可以让您在Mac上同时运行多个操作系统&#xff0c;为您提供更大的灵活性和兼容性。 Parallels Desktop 19 for Mac(PD19虚拟机)下载安装包 Parallels Desktop 19 for Mac(PD19虚拟机)详细图…

【Visual Studio】Visual Studio使用技巧及报错解决合集

目录 目录 一.概述 二.Visual Studio报错问题及解决方法 三.Visual Studio操作过程中遇到的问题及解决方法 四.Visual Studio编译优化选项 五.Visual Studio快捷键 一.概述 持续更新Visual Studio报错及解决方法&#xff0c;包括Visual Studio报错问题及解决方法、Visua…

phenocycler(原CODEX)鉴定三阴乳腺癌不同的治疗反应轨迹

目前临床上需要制定策略来更好地识别哪些患者可以从单独的免疫疗法中受益&#xff0c;或者哪些患者可能需要化疗或放疗等额外疗法来克服耐药性。尽管一些联合放射治疗&#xff08;RT&#xff09;和免疫检查点抑制&#xff08;ICI&#xff09;的临床研究取得了成功&#xff0c;但…

【Linux】常见指令(下)

【Linux】常见指令&#xff08;下&#xff09; 通配符 *man指令cp指令echo指令cat指令&#xff08;简单介绍&#xff09;cp指令 mv指令alias指令which ctrl ccat指令linux下一切皆文件 more指令less指令head指令tail指令管道 通配符 ‘*’ 通配符’ *‘&#xff0c;是可以匹配…

怎样去除视频上的水印和文字,视频水印文本移除教程

在观看和分享视频时&#xff0c;我们经常会遇到带有水印或额外文字的情况。这些标记有时是为了版权保护&#xff0c;有时则是平台的标识&#xff0c;但在某些情况下&#xff0c;它们可能会干扰视频的观赏体验。本文将向你介绍常见的视频水印类型以及如何使用简鹿水印助手去除这…

ARM功耗管理之功耗数据与功耗收益评估

安全之安全(security)博客目录导读 思考&#xff1a;功耗数据如何测试&#xff1f;功耗曲线&#xff1f;功耗收益评估&#xff1f; UPF的全称是Unified Power Format&#xff0c;其作用是把功耗设计意图&#xff08;power intent&#xff09;传递给EDA工具&#xff0c; 从而帮…

05 以物品与用户为基础个性化推荐算法的四大策略

《易经》&#xff1a;“九二&#xff1a;见龙在田&#xff0c;利见大人”。九二是指阳爻在卦中处于第二位&#xff0c;见龙指龙出现在地面上&#xff0c;开始崭露头角&#xff0c;但是仍须努力&#xff0c;应处于安于偏下的位置。 本节是模块二第一节&#xff0c;模块二讲解传…

[Linux+git+Gitee+Jenkins]持续集成实验安装配置详细

首先理解持续集成原理&#xff0c;看懂并理解图 1。 图 1 持续集成原理结构 图 1 中&#xff0c;版本控制服务器指远程代码仓库&#xff0c;本实验使用 GitEE 作为远程代码仓库&#xff1b;Jenkins 自动化部署服务器为虚拟机&#xff0c;操作系统为 Linux &#xff1b…

人工智能与伦理挑战:多维度应对策略

人工智能技术近年来取得了迅猛发展&#xff0c;广泛应用于医疗诊断、金融分析、教育辅助、自动驾驶等各个领域&#xff0c;极大地提升了生产效率和服务质量&#xff0c;推动了科技进步和商业创新。然而&#xff0c;伴随其普及和应用的泛滥&#xff0c;AI也带来了数据隐私侵犯、…

C语言 | Leecode C语言题解之第229题多数元素II

题目&#xff1a; 题解&#xff1a; /*** Note: The returned array must be malloced, assume caller calls free().*//*假定 num1&#xff0c;num2 为出现次数大于 nums.length / 3 的两个数。&#xff08;最多出现两个&#xff09;遍历 nums&#xff0c; 若出现 num1、num2…

2024年高职云计算实验室建设及云计算实训平台整体解决方案

随着云计算技术的飞速发展&#xff0c;高职院校亟需构建一个与行业需求紧密结合的云计算实验室和实训平台。以下是针对2024年高职院校云计算实验室建设的全面解决方案。 1、在高职云计算实验室的建设与规划中&#xff0c;首要任务是立足于云计算学科的精准定位&#xff0c;紧密…

如何通过SSH协议使用WinSCP实现Windows与Linux之间的远程公网文件传输

目录 ⛳️推荐 前言 1. Windows传输文件至Linux 2. WinSCP使用公网TCP地址连接 3. WinSCP使用固定公网TCP地址访问服务器 ⛳️推荐 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站 前…

2.5 计算机网络

声明&#xff1a;文章参考的《系统架构设计师教程&#xff08;第二版&#xff09;》&#xff0c;如有侵权&#xff0c;本人将立即修改和删除。 利用通信线路将地理上分散的、具有独立功能的计算机系统和通信设备按不同的形式连接起来&#xff0c;并依靠网络软件以及通信协议实现…

深入Python网络编程:基础、工具和实践

深入Python网络编程&#xff1a;基础、工具和实践 网络编程是Python应用领域中的一个强大且核心的部分&#xff0c;它为开发者提供了与互联网或其他网络设备进行交互的能力。无论是构建Web服务、APIs&#xff0c;还是创建网络客户端&#xff0c;Python都提供了丰富的库来简化这…

【学习笔记】无人机(UAV)在3GPP系统中的增强支持(九)-无人机服务区分离

引言 本文是3GPP TR 22.829 V17.1.0技术报告&#xff0c;专注于无人机&#xff08;UAV&#xff09;在3GPP系统中的增强支持。文章提出了多个无人机应用场景&#xff0c;分析了相应的能力要求&#xff0c;并建议了新的服务级别要求和关键性能指标&#xff08;KPIs&#xff09;。…

Go语言--广播式并发聊天服务器

实现功能 每个客户端上线&#xff0c;服务端可以向其他客户端广播上线信息&#xff1b;发送的消息可以广播给其他在线的客户支持改名支持客户端主动退出支持通过who查找当前在线的用户超时退出 流程 变量 用户结构体 保存用户的管道&#xff0c;用户名以及网络地址信息 typ…

使用mybatis的statementHander拦截器监控表和字段并发送钉钉消息

新建mybatis的statementHander拦截器拦截器 类 面试题&#xff1a; 2.实现 解析Sql时引入JSqlParser JSqlParser 是一个 SQL 语句解析器。 它将 SQL转换为可遍历的 Java 类层次结构。 <dependency><groupId>com.github.jsqlparser</groupId><artifac…