AOP实现Restful接口操作日志入表方案
文章目录
- 前言
- 一、基础资源配置
- 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);});}@NonNullprivate 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;}@NonNullprivate 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);}@Generatedpublic 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;@Overridepublic <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和待消息对象对象OptLogDTOprivate 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@Generatedpublic SysLogListener(final Consumer<OptLogDTO> consumer) {this.consumer = consumer;}
}@Configuration
public class BaseWebConfiguration extends BaseConfig {@Beanpublic SysLogListener sysLogListener(BaseOperationLogServicelogApi) {//2 Lambda 方法实现类定义具体消费【即等待执行 实例化(消费)】return new SysLogListener(data -> logApi.save(BeanPlusUtil.toBean(data, BaseOperationLogSaveVO.class)));}
}执行:BaseOperationLogService.save(..开始向数据表插入日志记录..)
相关文章:
AOP实现Restful接口操作日志入表方案
文章目录 前言一、基础资源配置1.操作日志基本表[base_operation_log] 见附录1。2.操作日志扩展表[base_operation_log_ext] 见附录2。3.定义接口操作系统日志DTO:OptLogDTO4.定义操作日志注解类WebLog5.定义操作日志Aspect切面类SysLogAspect6.定义异步监听日志事件…...

【MATLAB去噪算法】基于CEEMDAN联合小波阈值去噪算法(第四期)
CEEMDAN联合小波阈值去噪算法相关文献 一、EMD 与 EEMD 的局限性 (1)EMD (经验模态分解) 旨在自适应地将非线性、非平稳信号分解成一系列 本征模态函数 (IMFs),这些 IMFs 从高频到低频排列。 核心问题:模态混合 (Mode Mixing) 同…...
Webhook 配置备忘
本文地址:blog.lucien.ink/archives/552 将下列代码保存为 install.sh,然后 bash install.sh。 #!/usr/bin/env bash set -e wget https://github.mirrors.lucien.ink/https://github.com/adnanh/webhook/releases/download/2.8.2/webhook-linux-amd64.…...

从理论崩塌到新路径:捷克科学院APL Photonics论文重构涡旋光技术边界
理论预言 vs 实验挑战 光子轨道角动量(Orbital Angular Momentum, OAM)作为光场调控的新维度,曾被理论预言可突破传统拉曼散射的对称性限制——尤其是通过涡旋光(如拉盖尔高斯光束)激发晶体中常规手段无法探测的"…...
机器学习笔记【Week7】
一、SVM的动机:大间隔分类器 1、逻辑回归回顾 假设函数为 sigmoid 函数: h θ ( x ) 1 1 e − θ T x h_\theta(x) \frac{1}{1 e^{-\theta^Tx}} hθ(x)1e−θTx1 分类依据是 h θ ( x ) ≥ 0.5 h_\theta(x) \geq 0.5 hθ(x)≥0.5 为正类&a…...
LSM Tree算法原理
LSM Tree(Log-Structured Merge Tree)是一种针对写密集型场景优化的数据结构,广泛应用于LevelDB、RocksDB等数据库引擎中。其核心原理如下: 1. 写入优化:顺序写代替随机写 内存缓冲(MemTable):写入操作首先被写入内存中的数据结构(如跳表或平衡树),…...

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

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

固态继电器与驱动隔离器:电力系统的守护者
在电力系统中, 固态继电器合驱动隔离器像两位“电力守护神”,默默地确保电力设备的安全与稳定运行。它们通过高效、可靠的性能,保障了电力设备在各种环境下的正常工作。 固态继电器是电力控制中的关键组成部分,利用半导体器件来实…...
uni-app 如何实现选择和上传非图像、视频文件?
在 uni-app 中实现选择和上传非图像、视频文件,可根据不同端(App、H5、小程序)的特点,采用以下方法: 一、通用思路(多端适配优先推荐) 借助 uni.chooseFile 选择文件,再用 uni.upl…...
区块链架构深度解析:从 Genesis Block 到 Layer 2
# 区块链架构深度解析:从 Genesis Block 到 Layer 2 目录 一、Genesis Block:区块链的起点 二、Layer 0:区块链的底层网络架构 三、Layer 1:核心协议层 🚀 四、Layer 2:扩展性解决方案 五、未来展望&a…...

【数据分析】基于adonis2与pairwise.adonis2的群组差异分析教程
禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍加载R包数据下载导入数据数据预处理adonis分析pairwise.adonis2分析总结系统信息介绍 本教程主要用于执行和分析基于距离矩阵的多样性和群落结构分析,特别是通过adonis2和pairwi…...
使用pdm+uv替换poetry
用了好几年poetry了,各方面都还挺满意,就是lock实在太慢; 已经试用pdmuv一段时间了,确实是快,也基本能覆盖poetry的功能。 至于为什么用pdmuv,而不是只用uv,原因很多,有兴趣的可以…...

Nginx + Tomcat负载均衡群集
目录 一、案例环境 二、部署 Tomcat(102/103) 1、准备环境 (1)关闭firewalld 防火墙 (2)安装JDK 2、安装配置 Tomcat (1)Tomcat 的安装和配置 (2)移动…...

嵌入式开发之STM32学习笔记day22
STM32F103C8T6 FLASH闪存 1 FLASH简介 STM32F1系列微控制器的FLASH存储器是一种非易失性存储器,它在微控制器中扮演着至关重要的角色。以下是对STM32F1系列FLASH存储器及其相关编程方式的扩展说明: 【FLASH存储器的组成部分】 程序存储器:这…...

分词算法BBPE详解和Qwen的应用
一、TL;DR BPE有什么问题:依旧会遇到OOV问题,并且中文、日文这些大词汇表模型容易出现训练中未出现过的字符Byte-level BPE怎么解决:与BPE一样是高频字节进行合并,但BBPE是以UTF-8编码UTF-8编码字节序列而非字符序列B…...
关于ETL的BackgroundScheduler同步方案和misfire_grace_time
如果做ETL避免脏数据,那么不可以允许同一个job有并行允许的情况,也就是说max_instance参数始终设置成1。 这时候执行ETL任务,会有以下情况。 1 任务不超时。正常执行 2 任务超时。如果下一个时间点上一次任务还没有执行完,那么…...

多线程下使用缓存+锁Lock, 出现“锁失效” + “缓存未命中竞争”的缓存击穿情况,双重检查缓存解决问题
多线程情况下,想通过缓存同步锁的机制去避免多次重复处理逻辑,尤其是I/0操作,但是在实际的操作过程中发现多次访问的日志 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服务器
昨天留了一个小尾巴~~就是在命令行或者代码调用命令,以TLS/SSL显式加密方式,访问FTP服务器,上传和下载文件。 有小伙伴可能说ftp命令不可以吗?不可以哦~~ ftp 命令本身不支持显式加密。要实现 FTP 的显式加密,可以使…...
Mac 双系统
准备——Windows10 ISO文件下载 下载地址:https://msdn.itellyou.cn 操作系统 Win10-1903镜像 复制链接迅雷下载 第一步——查看系统磁盘剩余空间 打开“启动台”找到“其他”文件夹,打开“磁盘工具”(剩余空间要大于40GB) 第二…...

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…...
SpringBoot 系列之集成 RabbitMQ 实现高效流量控制
系列博客专栏: JVM系列博客专栏SpringBoot系列博客 Spring Boot 2.2.1 集成 RabbitMQ 实现高效流量控制 在分布式系统中,消息队列是实现异步通信、解耦服务的重要组件。RabbitMQ 作为一款成熟的开源消息队列,广泛应用于各类项目中。本文将…...

LLaMA-Factory和python版本的兼容性问题解决
引言 笔者今天在电脑上安装下LLaMA-Factory做下本地的模型调优。 从github上拉取代码git clone https://github.com/hiyouga/LLaMA-Factory.git. pycharm建立工程,按照官网指导如下: LLaMA-Factory 安装 在安装 LLaMA-Factory 之前,请确保您安装了下列依赖: 运行以…...
掌握子网划分:优化IP分配与管理
子网划分是通过调整子网掩码,将单一IP网络划分为多个逻辑子网的过程,其核心原理是借用主机位作为子网位以优化地址分配和管理。具体方法与原理如下: 一、子网划分基本原理 核心目的: 减少IP浪费:避免大块地址闲置&…...
Linux中shell编程表达式和数组讲解
一、表达式 1.1 测试表达式 样式1: test 条件表达式 样式2: [ 条件表达式 ] 注意:以上两种方法的作用完全一样,后者为常用。但后者需要注意方括号[、]与条件表达式之间至少有一个空格。test跟 [] 的意思一样条件成立,状态返回值是0条件不成…...

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

分布式锁-Redisson实现
目录 本地锁的局限性 Redisson解决分布式锁问题 在分布式环境下,分布式锁可以保证在多个节点上的并发操作时数据的一致性和互斥性。分布式锁有多种实现方案,最常用的两种方案是:zookeeper和redis,本文介绍redis实现分布式锁方案…...
HTTP 请求协议简单介绍
目录 常见的 HTTP 响应头字段 Java 示例代码:发送 HTTP 请求并处理响应 代码解释: 运行结果: 文件名: 总结: HTTP(HyperText Transfer Protocol)是用于客户端与服务器之间通信的协议。它定…...

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_…...