AOP实现Restful接口操作日志入表方案

news2025/6/7 19:30:44

文章目录

  • 前言
  • 一、基础资源配置
      • 1.操作日志基本表[base_operation_log] 见附录1。
      • 2.操作日志扩展表[base_operation_log_ext] 见附录2。
      • 3.定义接口操作系统日志DTO:OptLogDTO
      • 4.定义操作日志注解类WebLog
      • 5.定义操作日志Aspect切面类SysLogAspect
      • 6.定义异步监听日志事件类SysLogListener
      • 8.定义系统日志事件SysLogEvent类
      • 9.Web配置初始化SysLogListener
      • 10. 资源实体内接口注入@WebLog
  • 附录


前言

构建已上线系统, 以@注解方式对所有资源Restful标准接口注入,将资源Restful接口操作记录存在到操作日志基本表[base_operation_log记录接口操作详情如操作IP、操作人、请求方法 等等]操作日志扩展表[base_operation_log_ext记录接口的请求参数、响应体和异常描述],减少代码的侵入。

一、基础资源配置

1.操作日志基本表[base_operation_log] 见附录1。

2.操作日志扩展表[base_operation_log_ext] 见附录2。

3.定义接口操作系统日志DTO:OptLogDTO

记录所有资源实体的操作日志信息实。

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode
@Accessors(chain = true)
public class OptLogDTO {
    private static final long serialVersionUID = 1L;
    /**
     * 操作IP
     */
    private String requestIp;
    private Long basePoolNameHeader;
    private Long extendPoolNameHeader;
    /**
     * 日志链路追踪id日志标志
     */
    private String trace;
    /**
     * 日志类型
     * #LogType{OPT:操作类型;EX:异常类型}
     */
    private String type;
    /**
     * 操作人
     */
    private String userName;
    /**
     * 操作描述
     */
    private String description;
    /**
     * 类路径
     */
    private String classPath;
    /**
     * 请求类型
     */
    private String actionMethod;
    /**
     * 请求地址
     */
    private String requestUri;
    /**
     * 请求类型
     * #HttpMethod{GET:GET请求;POST:POST请求;PUT:PUT请求;DELETE:DELETE请求;PATCH:PATCH请求;TRACE:TRACE请求;HEAD:HEAD请求;OPTIONS:OPTIONS请求;}
     */
    private String httpMethod;
    /**
     * 请求参数
     */
    private String params;
    /**
     * 返回值
     */
    private String result;
    /**
     * 异常描述
     */
    private String exDetail;
    /**
     * 开始时间
     */
    private LocalDateTime startTime;
    /**
     * 完成时间
     */
    private LocalDateTime finishTime;
    /**
     * 消耗时间
     */
    private Long consumingTime;
    /**
     * 浏览器
     */
    private String ua;
    private Long createdBy;
    private Long createdOrgId;
    private String token;
}

4.定义操作日志注解类WebLog

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WebLog {
    /**
     * 是否启用 操作日志
     * 禁用控制优先级:lamp.log.enabled = false > 控制器类上@WebLog(enabled = false) > 控制器方法上@WebLog(enabled = false)
     */
    boolean enabled() default true;

    /**
     * 操作日志的描述, 支持spring 的 SpEL 表达式。
     * @return {String}
     */
    String value() default "";
    /**
     * 模块
     */
    String modular() default "";

    /**
     * 是否拼接Controller类上@Api注解的描述值
     * @return 是否拼接Controller类上的描述值
     */
    boolean controllerApiValue() default true;

    /**
     * 是否记录方法的入参
     */
    boolean request() default true;

    /**
     * 若设置了 request = false、requestByError = true,则方法报错时,依然记录请求的入参
     * @return 当 request = false时, 方法报错记录请求参数
     */
    boolean requestByError() default true;

    /**
     * 是否记录返回值
     */
    boolean response() default true;
}

5.定义操作日志Aspect切面类SysLogAspect

1.定义controller切入点拦截规则:拦截标记WebLog注解和指定包下的方法
2.在接口方法三个时刻点[返回通知doAfterReturning、异常通知doAfterThrowable、执行方法之前doBefore] 进行拦截,构建OptLogDTO对象并发布订阅事件 publishEvent(sysLogDTO)

package top.tangyh.basic.log.aspect;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.extra.servlet.JakartaServletUtil;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
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.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.MDC;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.lang.NonNull;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import top.tangyh.basic.annotation.log.WebLog;
import top.tangyh.basic.base.R;
import top.tangyh.basic.context.ContextConstants;
import top.tangyh.basic.context.ContextUtil;
import top.tangyh.basic.jackson.JsonUtil;
import top.tangyh.basic.log.event.SysLogEvent;
import top.tangyh.basic.log.util.LogUtil;
import top.tangyh.basic.log.util.ThreadLocalParam;
import top.tangyh.basic.model.log.OptLogDTO;
import top.tangyh.basic.utils.SpringUtils;
import top.tangyh.basic.utils.StrPool;

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Objects;
import java.util.function.Consumer;

/**
 * 操作日志使用spring event异步入库
 */
@Slf4j
@Aspect
public class SysLogAspect {

    public static final int MAX_LENGTH = 65535;
    private static final ThreadLocal<OptLogDTO> THREAD_LOCAL = new ThreadLocal<>();
    private static final String FORM_DATA_CONTENT_TYPE = "multipart/form-data";
    /**
     * 用于SpEL表达式解析.
     */
    private final SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
    /**
     * 用于获取方法参数定义名字.
     */
    private final DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();

    /***
     * 定义controller切入点拦截规则:拦截标记WebLog注解和指定包下的方法
     * 2个表达式加起来才能拦截所有Controller 或者继承了BaseController的方法
     *
     * execution(public * top.tangyh.basic.base.controller.*.*(..)) 解释:
     * 第一个* 任意返回类型
     * 第二个* top.tangyh.basic.base.controller包下的所有类
     * 第三个* 类下的所有方法
     * ()中间的.. 任意参数
     *
     * \@annotation(top.tangyh.basic.annotation.log.WebLog) 解释:
     * 标记了@WebLog 注解的方法
     */
    @Pointcut("execution(public * top.tangyh.basic.base.controller.*.*(..)) || @annotation(top.tangyh.basic.annotation.log.WebLog)")
    public void sysLogAspect() {

    }

    /**
     * 返回通知doAfterReturning
     *
     * @param ret       返回值
     * @param joinPoint 端点
     */
    @AfterReturning(returning = "ret", pointcut = "sysLogAspect()")
    public void doAfterReturning(JoinPoint joinPoint, Object ret) {
        tryCatch(p -> {
            WebLog sysLog = LogUtil.getTargetAnnotation(joinPoint);
            if (check(joinPoint, sysLog)) {
                return;
            }

            R r = Convert.convert(R.class, ret);
            OptLogDTO sysLogDTO = get();
            if (r == null) {
                sysLogDTO.setType("OPT");
                if (sysLog.response()) {
                    sysLogDTO.setResult(getText(String.valueOf(ret == null ? StrPool.EMPTY : ret)));
                }
            } else {
                if (r.getIsSuccess()) {
                    sysLogDTO.setType("OPT");
                } else {
                    sysLogDTO.setType("EX");
                    sysLogDTO.setExDetail(r.getMsg());
                }
                if (sysLog.response()) {
                    sysLogDTO.setResult(getText(r.toString()));
                }
            }

            publishEvent(sysLogDTO);
        });

    }

    /**
     * 异常通知doAfterThrowable
     *
     * @param joinPoint 端点
     * @param e         异常
     */
    @AfterThrowing(pointcut = "sysLogAspect()", throwing = "e")
    public void doAfterThrowable(JoinPoint joinPoint, Throwable e) {
        tryCatch((aaa) -> {
            WebLog sysLog = LogUtil.getTargetAnnotation(joinPoint);
            if (check(joinPoint, sysLog)) {
                return;
            }

            OptLogDTO optLogDTO = get();
            optLogDTO.setType("EX");

            // 遇到错误时,请求参数若为空,则记录
            if (!sysLog.request() && sysLog.requestByError() && StrUtil.isEmpty(optLogDTO.getParams())) {
                Object[] args = joinPoint.getArgs();
                HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
                String strArgs = getArgs(args, request);
                optLogDTO.setParams(getText(strArgs));
            }

            // 异常对象
            optLogDTO.setExDetail(ExceptionUtil.stacktraceToString(e, MAX_LENGTH));

            publishEvent(optLogDTO);
        });
    }

    /**
     * 执行方法之前doBefore
     *
     * @param joinPoint 端点
     */
    @Before(value = "sysLogAspect()")
    public void doBefore(JoinPoint joinPoint) {
        tryCatch(val -> {
            WebLog sysLog = LogUtil.getTargetAnnotation(joinPoint);
            if (check(joinPoint, sysLog)) {
                return;
            }
            OptLogDTO optLogDTO = buildOptLogDTO(joinPoint, sysLog);
            THREAD_LOCAL.set(optLogDTO);
        });
    }

    @NonNull
    private OptLogDTO buildOptLogDTO(JoinPoint joinPoint, WebLog sysLog) {
        // 开始时间
        OptLogDTO optLogDTO = get();
        optLogDTO.setCreatedBy(ContextUtil.getUserId());
        setDescription(joinPoint, sysLog, optLogDTO);
        // 类名
        optLogDTO.setClassPath(joinPoint.getTarget().getClass().getName());
        //获取执行的方法名
        optLogDTO.setActionMethod(joinPoint.getSignature().getName());

        HttpServletRequest request = setParams(joinPoint, sysLog, optLogDTO);
        optLogDTO.setRequestIp(JakartaServletUtil.getClientIP(request));
        optLogDTO.setRequestUri(URLUtil.getPath(request.getRequestURI()));
        optLogDTO.setHttpMethod(request.getMethod());
        optLogDTO.setUa(StrUtil.sub(request.getHeader("user-agent"), 0, 500));
        if (ContextUtil.getBoot()) {
            optLogDTO.setCreatedOrgId(ContextUtil.getCurrentCompanyId());
            optLogDTO.setToken(ContextUtil.getToken());
        } else {
            optLogDTO.setToken(Convert.toStr(request.getHeader(ContextConstants.TOKEN_HEADER)));
            optLogDTO.setCreatedOrgId(Convert.toLong(request.getHeader(ContextConstants.CURRENT_COMPANY_ID_HEADER)));
        }
        optLogDTO.setTrace(MDC.get(ContextConstants.TRACE_ID_HEADER));
        if (StrUtil.isEmpty(optLogDTO.getTrace())) {
            optLogDTO.setTrace(request.getHeader(ContextConstants.TRACE_ID_HEADER));
        }
        optLogDTO.setStartTime(LocalDateTime.now());
        return optLogDTO;
    }

    @NonNull
    private HttpServletRequest setParams(JoinPoint joinPoint, WebLog sysLog, OptLogDTO optLogDTO) {
        // 参数
        Object[] args = joinPoint.getArgs();

        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes(), "只能在Spring Web环境使用@WebLog记录日志")).getRequest();
        if (sysLog.request()) {
            String strArgs = getArgs(args, request);
            optLogDTO.setParams(getText(strArgs));
        }
        return request;
    }

    private void setDescription(JoinPoint joinPoint, WebLog sysLog, OptLogDTO optLogDTO) {
        String controllerDescription = "";
        Tag api = joinPoint.getTarget().getClass().getAnnotation(Tag.class);
        if (api != null) {
            controllerDescription = api.name();
        }

        String controllerMethodDescription = LogUtil.getDescribe(sysLog);

        if (StrUtil.isNotEmpty(controllerMethodDescription) && StrUtil.contains(controllerMethodDescription, StrPool.HASH)) {
            //获取方法参数值
            Object[] args = joinPoint.getArgs();

            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            controllerMethodDescription = getValBySpEl(controllerMethodDescription, methodSignature, args);
        }

        if (StrUtil.isEmpty(controllerDescription)) {
            optLogDTO.setDescription(controllerMethodDescription);
        } else {
            if (sysLog.controllerApiValue()) {
                optLogDTO.setDescription(controllerDescription + "-" + controllerMethodDescription);
            } else {
                optLogDTO.setDescription(controllerMethodDescription);
            }
        }
    }

    private OptLogDTO get() {
        OptLogDTO sysLog = THREAD_LOCAL.get();
        if (sysLog == null) {
            return new OptLogDTO();
        }
        return sysLog;
    }

    private void tryCatch(Consumer<String> consumer) {
        try {
            consumer.accept("");
        } catch (Exception e) {
            log.warn("记录操作日志异常", e);
            THREAD_LOCAL.remove();
        }
    }

    private void publishEvent(OptLogDTO sysLog) {
        sysLog.setFinishTime(LocalDateTime.now());
        sysLog.setConsumingTime(sysLog.getStartTime().until(sysLog.getFinishTime(), ChronoUnit.MILLIS));
        SpringUtils.publishEvent(new SysLogEvent(sysLog));
        THREAD_LOCAL.remove();
    }

    /**
     * 监测是否需要记录日志
     *
     * @param joinPoint 端点
     * @param sysLog    操作日志
     * @return true 表示不需要记录日志
     */
    private boolean check(JoinPoint joinPoint, WebLog sysLog) {
        if (sysLog == null || !sysLog.enabled()) {
            return true;
        }
        // 读取目标类上的注解
        WebLog targetClass = joinPoint.getTarget().getClass().getAnnotation(WebLog.class);
        // 加上 sysLog == null 会导致父类上的方法永远需要记录日志
        return targetClass != null && !targetClass.enabled();
    }

    /**
     * 截取指定长度的字符串
     *
     * @param val 参数
     * @return 截取文本
     */
    private String getText(String val) {
        return StrUtil.sub(val, 0, 65535);
    }

    private String getArgs(Object[] args, HttpServletRequest request) {
        String strArgs = StrPool.EMPTY;
        Object[] params = Arrays.stream(args).filter(item -> !(item instanceof ServletRequest || item instanceof ServletResponse)).toArray();

        try {
            if (!request.getContentType().contains(FORM_DATA_CONTENT_TYPE)) {
                strArgs = JsonUtil.toJson(params);
            }
        } catch (Exception e) {
            try {
                strArgs = Arrays.toString(params);
            } catch (Exception ex) {
                log.warn("解析参数异常", ex);
            }
        }
        return strArgs;
    }

    /**
     * 解析spEL表达式
     */
    private String getValBySpEl(String spEl, MethodSignature methodSignature, Object[] args) {
        try {
            //获取方法形参名数组
            String[] paramNames = nameDiscoverer.getParameterNames(methodSignature.getMethod());
            if (paramNames != null && paramNames.length > 0) {
                Expression expression = spelExpressionParser.parseExpression(spEl);
                // spring的表达式上下文对象
                EvaluationContext context = new StandardEvaluationContext();
                // 给上下文赋值
                for (int i = 0; i < args.length; i++) {
                    context.setVariable(paramNames[i], args[i]);
                    context.setVariable("p" + i, args[i]);
                }
                ThreadLocalParam tlp = new ThreadLocalParam();
                BeanUtil.fillBeanWithMap(ContextUtil.getLocalMap(), tlp, true);
                context.setVariable("threadLocal", tlp);
                Object value = expression.getValue(context);
                return value == null ? spEl : value.toString();
            }
        } catch (Exception e) {
            log.warn("解析操作日志的el表达式出错", e);
        }
        return spEl;
    }
}

6.定义异步监听日志事件类SysLogListener

@EventListener({SysLogEvent.class})监听SysLogEvent事件,当有发布事件publishEvent(sysLogDTO)发生,根据事件对象SysLogEvent 获取事件关联资源体(即OptLogDTO sysLog = (OptLogDTO)event.getSource()),并开始执行消费this.consumer.accept(sysLog)

import java.util.function.Consumer;
import lombok.Generated;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Async;
public class SysLogListener {
    private final Consumer<OptLogDTO> consumer;
    @Async
    @Order
    @EventListener({SysLogEvent.class})
    public void saveSysLog(SysLogEvent event) {
        OptLogDTO sysLog = (OptLogDTO)event.getSource();
        ContextUtil.setToken(sysLog.getToken());
        this.consumer.accept(sysLog);
    }
    @Generated
    public SysLogListener(final Consumer<OptLogDTO> consumer) {
        this.consumer = consumer;
    }
}

8.定义系统日志事件SysLogEvent类

import org.springframework.context.ApplicationEvent;
import top.tangyh.basic.model.log.OptLogDTO;
public class SysLogEvent extends ApplicationEvent {
    public SysLogEvent(OptLogDTO source) {
        super(source);
    }
}

9.Web配置初始化SysLogListener

初始化SysLogListener监听类和消费动作的接口类BaseOperationLog-Service的保存(即消费事件资源)操作,
logApi.save(BeanPlusUtil.toBean(data, BaseOperationLogSaveVO.class)
其中:logApi为BaseOperationLogService 基础接口日志服务类

import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import top.tangyh.basic.boot.config.BaseConfig;
import top.tangyh.basic.constant.Constants;
import top.tangyh.basic.log.event.SysLogListener;
import top.tangyh.basic.utils.BeanPlusUtil;
import top.tangyh.lamp.base.service.system.BaseOperationLogService;
import top.tangyh.lamp.base.vo.save.system.BaseOperationLogSaveVO;
/**
 * 基础服务-Web配置SysLogListener 
 */
@Configuration
public class BaseWebConfiguration extends BaseConfig {
    /**
     * lamp.log.enabled = true 并且 lamp.log.type=DB时实例该类
     */
    @Bean
    @ConditionalOnExpression("$lamp.log.enabled:true} && 'DB'.equals('${lamp.log.type:DB}')")
    public SysLogListener sysLogListener(BaseOperationLogService logApi) {
        return new SysLogListener(data -> logApi.save(BeanPlusUtil.toBean(data, BaseOperationLogSaveVO.class)));
    }
}

yml配置:

lamp:
  log: # 详情看:OptLogProperties
    # 开启记录操作日志
    enabled: true
    # 记录到什么地方  DB:mysql LOGGER:日志文件
    type: DB
package top.tangyh.lamp.base.service.system.impl;

import cn.hutool.core.bean.BeanUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import top.tangyh.basic.base.service.impl.SuperServiceImpl;
import top.tangyh.lamp.base.entity.system.BaseOperationLog;
import top.tangyh.lamp.base.entity.system.BaseOperationLogExt;
import top.tangyh.lamp.base.manager.system.BaseOperationLogManager;
import top.tangyh.lamp.base.mapper.system.BaseOperationLogExtMapper;
import top.tangyh.lamp.base.service.system.BaseOperationLogService;
import top.tangyh.lamp.base.vo.result.system.BaseOperationLogResultVO;
import top.tangyh.lamp.base.vo.save.system.BaseOperationLogSaveVO;
import java.time.LocalDateTime;

/**
 * 业务实现类操作日志
 */
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class BaseOperationLogServiceImpl extends SuperServiceImpl<BaseOperationLogManager, Long, BaseOperationLog> implements BaseOperationLogService {
    private final BaseOperationLogExtMapper 
    baseOperationLogExtMapper;
    @Override
    public <SaveVO> BaseOperationLog save(SaveVO saveVO) {
        BaseOperationLogSaveVO logSaveVO = (BaseOperationLogSaveVO) saveVO;
        BaseOperationLogExt baseOperationLogExt = BeanUtil.toBean(saveVO, BaseOperationLogExt.class);
        //存储扩展日志表
        baseOperationLogExtMapper.insert(baseOperationLogExt);
        logSaveVO.setId(baseOperationLogExt.getId());
        return super.save(logSaveVO);//存储操作基础日志表
    }
}

10. 资源实体内接口注入@WebLog

@Slf4j
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/baseEmployee")
@Tag(name = "员工")
public class BaseEmployeeController ........ {
        //response  = false时日志扩展表不记录接口的response内容
        @WebLog(value = "分页列表查询", response = false)
    public R<IPage<BaseEmployeeResultVO>> page(@RequestBody
     @Validated PageParams<BaseEmployeePageQuery> params) {
        //todo:分页列表查询
        return R.success(page);
    }
}

附录

1.操作日志基本表[base_operation_log记录接口操作详情如操作IP、操作人、请求方法 等等]

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.Accessors;
import top.tangyh.basic.base.entity.Entity;
import top.tangyh.lamp.base.enumeration.system.LogType;
import top.tangyh.lamp.model.enumeration.HttpMethod;

import java.time.LocalDateTime;

import static top.tangyh.lamp.model.constant.Condition.LIKE;

/**
 * <p>
 * 实体类 操作日志
 */
@Data
@NoArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName("base_operation_log")
@AllArgsConstructor
public class BaseOperationLog extends Entity<Long> {

    private static final long serialVersionUID = 1L;

    /**
     * 操作IP
     */
    @TableField(value = "request_ip", condition = LIKE)
    private String requestIp;

    /**
     * 日志类型;#LogType{OPT:操作类型;EX:异常类型}
     */
    @TableField(value = "type")
    private LogType type;

    /**
     * 操作人
     */
    @TableField(value = "user_name", condition = LIKE)
    private String userName;

    /**
     * 操作描述
     */
    @TableField(value = "description", condition = LIKE)
    private String description;

    /**
     * 类路径
     */
    @TableField(value = "class_path", condition = LIKE)
    private String classPath;

    /**
     * 请求方法
     */
    @TableField(value = "action_method", condition = LIKE)
    private String actionMethod;

    /**
     * 请求地址
     */
    @TableField(value = "request_uri", condition = LIKE)
    private String requestUri;

    /**
     * 请求类型;#HttpMethod{GET:GET请求;POST:POST请求;PUT:PUT请求;DELETE:DELETE请求;PATCH:PATCH请求;TRACE:TRACE请求;HEAD:HEAD请求;OPTIONS:OPTIONS请求;}
     */
    @TableField(value = "http_method")
    private HttpMethod httpMethod;

    /**
     * 开始时间
     */
    @TableField(value = "start_time")
    private LocalDateTime startTime;

    /**
     * 完成时间
     */
    @TableField(value = "finish_time")
    private LocalDateTime finishTime;

    /**
     * 消耗时间
     */
    @TableField(value = "consuming_time")
    private Long consumingTime;

    /**
     * 浏览器
     */
    @TableField(value = "ua", condition = LIKE)
    private String ua;

    /**
     * 创建人组织
     */
    @TableField(value = "created_org_id")
    private Long createdOrgId;
}

2.操作日志扩展表[base_operation_log_ext记录接口的请求参数、响应体和异常描述]。

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.Accessors;
import top.tangyh.basic.base.entity.Entity;

import static top.tangyh.lamp.model.constant.Condition.LIKE;

/**
 * <p>
 * 实体类操作扩展日志
 */
@Data
@NoArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName("base_operation_log_ext")
@AllArgsConstructor
public class BaseOperationLogExt extends Entity<Long> {

    private static final long serialVersionUID = 1L;


    /**
     * 请求参数
     */
    @TableField(value = "params", condition = LIKE)
    private String params;

    /**
     * 返回值
     */
    @TableField(value = "result", condition = LIKE)
    private String result;

    /**
     * 异常描述
     */
    @TableField(value = "ex_detail", condition = LIKE)
    private String exDetail;
}

3.其它工具类参考 lamp-util

4.注意Consumer ‌的使用。
Consumer用于定义对单个输入参数进行操作且不返回结果的任务‌,核心方法是 void accept(T t),执行操作逻辑。Consumer 就是一个“吃掉”(消费)一个对象 T 并对其做点事情(如修改、打印、存储等)但不吐回(返回)任何东西的工具。

public class SysLogListener {

    //1.1定义Consumer和待消息对象对象OptLogDTO
    private final Consumer<OptLogDTO> consumer;

    @Async
    @Order
    //监听到事件,开始执行消费操作
    @EventListener({SysLogEvent.class})
    public void saveSysLog(SysLogEvent event) {
        OptLogDTO sysLog = (OptLogDTO)event.getSource();
        ContextUtil.setToken(sysLog.getToken());
        //3 抽象方法,等待执行 实例化(消费)的操作
        this.consumer.accept(sysLog);
    }
    
    //1.2 构建Consumer和待消息对象OptLogDTO
    @Generated
    public SysLogListener(final Consumer<OptLogDTO> consumer) {
        this.consumer = consumer;
    }
}

@Configuration
public class BaseWebConfiguration extends BaseConfig {
    @Bean
    public SysLogListener sysLogListener(
    BaseOperationLogService
    logApi) {
    //2  Lambda 方法实现类定义具体消费【即等待执行 实例化(消费)】
        return new SysLogListener(data -> 
        logApi.save(BeanPlusUtil.toBean(data, 
        BaseOperationLogSaveVO.class)));
    }
}

执行:BaseOperationLogService.save(..开始向数据表插入日志记录..)

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

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

相关文章

【MATLAB去噪算法】基于CEEMDAN联合小波阈值去噪算法(第四期)

CEEMDAN联合小波阈值去噪算法相关文献 一、EMD 与 EEMD 的局限性 &#xff08;1&#xff09;EMD (经验模态分解) 旨在自适应地将非线性、非平稳信号分解成一系列 本征模态函数 (IMFs)&#xff0c;这些 IMFs 从高频到低频排列。 核心问题&#xff1a;模态混合 (Mode Mixing) 同…

从理论崩塌到新路径:捷克科学院APL Photonics论文重构涡旋光技术边界

理论预言 vs 实验挑战 光子轨道角动量&#xff08;Orbital Angular Momentum, OAM&#xff09;作为光场调控的新维度&#xff0c;曾被理论预言可突破传统拉曼散射的对称性限制——尤其是通过涡旋光&#xff08;如拉盖尔高斯光束&#xff09;激发晶体中常规手段无法探测的"…

智能推荐系统:协同过滤与深度学习结合

智能推荐系统&#xff1a;协同过滤与深度学习结合 系统化学习人工智能网站&#xff08;收藏&#xff09;&#xff1a;https://www.captainbed.cn/flu 文章目录 智能推荐系统&#xff1a;协同过滤与深度学习结合摘要引言技术原理对比1. 协同过滤算法&#xff1a;基于相似性的推…

文档处理组件Aspose.Words 25.5全新发布 :六大新功能与性能深度优化

在数字化办公日益普及的今天&#xff0c;文档处理的效率与质量直接影响到企业的运营效率。Aspose.Words 作为业界领先的文档处理控件&#xff0c;其最新发布的 25.5 版本带来了六大新功能和多项性能优化&#xff0c;旨在为开发者和企业用户提供更强大、高效的文档处理能力。 六…

固态继电器与驱动隔离器:电力系统的守护者

在电力系统中&#xff0c; 固态继电器合驱动隔离器像两位“电力守护神”&#xff0c;默默地确保电力设备的安全与稳定运行。它们通过高效、可靠的性能&#xff0c;保障了电力设备在各种环境下的正常工作。 固态继电器是电力控制中的关键组成部分&#xff0c;利用半导体器件来实…

【数据分析】基于adonis2与pairwise.adonis2的群组差异分析教程

禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍加载R包数据下载导入数据数据预处理adonis分析pairwise.adonis2分析总结系统信息介绍 本教程主要用于执行和分析基于距离矩阵的多样性和群落结构分析,特别是通过adonis2和pairwi…

Nginx + Tomcat负载均衡群集

目录 一、案例环境 二、部署 Tomcat&#xff08;102/103&#xff09; 1、准备环境 &#xff08;1&#xff09;关闭firewalld 防火墙 &#xff08;2&#xff09;安装JDK 2、安装配置 Tomcat &#xff08;1&#xff09;Tomcat 的安装和配置 &#xff08;2&#xff09;移动…

嵌入式开发之STM32学习笔记day22

STM32F103C8T6 FLASH闪存 1 FLASH简介 STM32F1系列微控制器的FLASH存储器是一种非易失性存储器&#xff0c;它在微控制器中扮演着至关重要的角色。以下是对STM32F1系列FLASH存储器及其相关编程方式的扩展说明&#xff1a; 【FLASH存储器的组成部分】 程序存储器&#xff1a;这…

分词算法BBPE详解和Qwen的应用

一、TL&#xff1b;DR BPE有什么问题&#xff1a;依旧会遇到OOV问题&#xff0c;并且中文、日文这些大词汇表模型容易出现训练中未出现过的字符Byte-level BPE怎么解决&#xff1a;与BPE一样是高频字节进行合并&#xff0c;但BBPE是以UTF-8编码UTF-8编码字节序列而非字符序列B…

多线程下使用缓存+锁Lock, 出现“锁失效” + “缓存未命中竞争”的缓存击穿情况,双重检查缓存解决问题

多线程情况下&#xff0c;想通过缓存同步锁的机制去避免多次重复处理逻辑&#xff0c;尤其是I/0操作&#xff0c;但是在实际的操作过程中发现多次访问的日志 2025-06-05 17:30:27.683 [ForkJoinPool.commonPool-worker-3] INFO Rule - [vagueNameMilvusReacll,285] - embeddin…

Playwright 测试框架 - .NET

💖亲爱的技术爱好者们,热烈欢迎来到 Kant2048 的博客!我是 Thomas Kant,很开心能在CSDN上与你们相遇~💖 本博客的精华专栏: 【自动化测试】

命令行以TLS/SSL显式加密方式访问FTP服务器

昨天留了一个小尾巴~~就是在命令行或者代码调用命令&#xff0c;以TLS/SSL显式加密方式&#xff0c;访问FTP服务器&#xff0c;上传和下载文件。 有小伙伴可能说ftp命令不可以吗&#xff1f;不可以哦~~ ftp 命令本身不支持显式加密。要实现 FTP 的显式加密&#xff0c;可以使…

Linux配置yum 时间同步服务 关闭防火墙 关闭ESlinux

1、配置yum 1.1、Could not resolve host: mirrorlist.centos.org; 未知的错误 https://blog.csdn.net/fansfi/article/details/146369946?fromshareblogdetail&sharetypeblogdetail&sharerId146369946&sharereferPC&sharesourceRockandrollman&sharefr…

LLaMA-Factory和python版本的兼容性问题解决

引言 笔者今天在电脑上安装下LLaMA-Factory做下本地的模型调优。 从github上拉取代码git clone https://github.com/hiyouga/LLaMA-Factory.git. pycharm建立工程,按照官网指导如下: LLaMA-Factory 安装 在安装 LLaMA-Factory 之前&#xff0c;请确保您安装了下列依赖: 运行以…

每日算法-250605

每日算法 - 20240605 525. 连续数组 题目描述 给定一个二进制数组 nums , 找到含有相同数量的 0 和 1 的最长连续子数组&#xff0c;并返回该子数组的长度。 思路 前缀和 哈希表 解题过程 核心思想是将问题巧妙地转换为寻找和为特定值的子数组问题。 转换问题&#xff1a;我…

分布式锁-Redisson实现

目录 本地锁的局限性 Redisson解决分布式锁问题 在分布式环境下&#xff0c;分布式锁可以保证在多个节点上的并发操作时数据的一致性和互斥性。分布式锁有多种实现方案&#xff0c;最常用的两种方案是&#xff1a;zookeeper和redis&#xff0c;本文介绍redis实现分布式锁方案…

C++学习-入门到精通【14】标准库算法

C学习-入门到精通【14】标准库算法 目录 C学习-入门到精通【14】标准库算法一、对迭代器的最低要求迭代器无效 二、算法1.fill、fill_n、generate和generate_n2.equal、mismatch和lexicographical_compare3.remove、remove_if、remove_copy和remove_copy_if4.replace、replace_…

HarmonyOS运动语音开发:如何让运动开始时的语音播报更温暖

##鸿蒙核心技术##运动开发##Core Speech Kit&#xff08;基础语音服务&#xff09;# 前言 在运动类应用中&#xff0c;语音播报功能不仅可以提升用户体验&#xff0c;还能让运动过程更加生动有趣。想象一下&#xff0c;当你准备开始运动时&#xff0c;一个温暖的声音提醒你“…

vscode使用系列之快速生成html模板

一.欢迎来到我的酒馆 vscode&#xff0c;yyds! 目录 一.欢迎来到我的酒馆二.vscode下载安装1.关于vscode你需要知道2.开始下载安装 三.vscode快速创建html模板 二.vscode下载安装 1.关于vscode你需要知道 Q&#xff1a;为什么使用vscode? A&#xff1a;使用vscode写…

网页前端开发(基础进阶4--axios)

Ajax Ajax(异步的JavaScript和XML) 。 XML是可扩展标记语言&#xff0c;本质上是一种数据格式&#xff0c;可以用来存储复杂的数据结构。 可以通过Ajax给服务器发送请求&#xff0c;并获取服务器响应的数据。 Ajax采用异步交互&#xff1a;可以在不重新加载整个页面的情况下&am…