如何使用AOP来记录接口操作日志

256 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第8天,点击查看活动详情

前言

在日常开发过程中,我们写的最多的莫过于各式各样的CRUD接口,通常我们需要知道接口的调用状态、时间、结果,以便于我们发现问题的时候去排查问题,但是在接口的实现方法里面手动写sql去完成记录日志,这种做法显然是大可不必的,记录日志这种简单的事情,还是让我们交给AOP来实现吧。

AOP切面

熟悉AOP的小伙伴都知道,AOP是基于jdk的动态代理和cglib代理,其实我们使用最多的本地事务、切面注解的方法,都是基于cglib的,通过定义@Aspect 修饰的切面类,来对前、中、后期、环绕、异常的情况进行切入,这里我只需要定义@AfterReturning,在接口调用后记录操作日志。

定义@OpeLog注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OpeLog {

    /**
     * 操作模块
     */
    String operationType() default "";

    /**
     * 操作项(例:更新)
     */
    String operationItem() default "";

    /**
     * 操作内容(知道更新具体内容的 -【未审核】 改为【已审核】)不知道更新内容的 存储请求参数
     */
    String operationContent() default "";

    /**
     * 业务单号 - 字段名
     * @return
     */
    String[] businessNoField() default {};
}

定义切面类

·通过反射机制获取类的方法,并且获取方法的属性,在获取接口请求方式post、get不同的传参方式,拿到请求入参保存到数据库对应的reqParam字段上,最后将字段保存在operationLog对象里,调用insert方法进行保存。

@Aspect
@Slf4j
@Component
public class OperationLogAspect extends AbstractController {

    @Autowired
    private OperationLogMapper operationLogMapper;


    @AfterReturning(pointcut = "@annotation(com.xxx.annotation.OpeLog)", returning = "result")
    public void saveOperationLog(JoinPoint joinPoint, Object result) {
        try {
            //获取签名
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            //获取方法
            Method method = signature.getMethod();
            //通过方法获取注解信息
            OpeLog log = method.getAnnotation(OpeLog.class);
            String operationType = log.operationType();
            String operationItem = log.operationItem();
            String operationContent = log.operationContent();
            String[] businessNoField = log.businessNoField();
            OperationLog operationLog = new OperationLog();
            operationLog.setOperationType(operationType);
            operationLog.setOperationItem(operationItem);
            operationLog.setOperationContent(operationContent);
            //获取当前用户信息
            OpenSysUser currentUser = null;
            //获取请求参数
            Object[] args = joinPoint.getArgs();
            for (Object arg : args) {
                if (arg instanceof OpenSysUser) {
                    currentUser = (OpenSysUser) arg;
                    break;
                }
            }
            if (currentUser == null) {
                currentUser = this.getCurrentUser(false);
            }
            operationLog.setOrgNo(currentUser.getOrgNo());
            operationLog.setOrgName(currentUser.getOrgName());
            operationLog.setMainOrgName(currentUser.getMainOrgName());
            operationLog.setMainOrgNo(currentUser.getMainOrgNo());
            operationLog.setCreateUserNo(currentUser.getAccount());
            operationLog.setCreateUserName(currentUser.getName());
            operationLog.setCreateUserPhone(currentUser.getPhone());

            /**
             * 保存请求参数
             * 排除httpRequest和httpResponse
             */
            Stream<?> stream = ArrayUtils.isEmpty(args) ? Stream.empty() : Arrays.stream(args);
            List<Object> logArgs = stream
                    .filter(arg -> (!(arg instanceof HttpServletRequest) && !(arg instanceof HttpServletResponse) && !(arg instanceof OpenSysUser)))
                    .collect(Collectors.toList());
            String reqParams = JSON.toJSONString(logArgs);
            operationLog.setReqParams(reqParams);

            List<String> fieldList = new ArrayList<>(Arrays.asList(businessNoField));
            if (!StringUtil.hasText(operationLog.getOperationContent())) {
                fieldList.add("operationContent");
            }
            JSONArray jsonArray = JSONArray.parseArray(reqParams);
            for (int i = 0; i < fieldList.size(); i++) {
                for (int j = 0; j < jsonArray.size(); j++) {
                    String fieldName = fieldList.get(i);
                    Object obj = jsonArray.get(j);
                    if (obj != null) {
                        if (obj instanceof JSON) {
                            String jsonString = JSON.toJSONString(obj);
                            JSONObject jsonObject = JSON.parseObject(jsonString);
                            Object value = jsonObject.get(fieldName);
                            //设置属性值
                            this.setValue(i, operationLog, fieldName, value);
                        } else {
                            //设置属性值
                            this.setValue(i, operationLog, fieldName, obj);
                            // 因为get请求是按照属性的顺序设值的,所以设值完删掉该属性
                            jsonArray.remove(j);
                            break;
                        }
                    }
                }
            }

            //保存
            this.operationLogMapper.insert(operationLog);
        } catch (Exception e) {
            log.error("保存日志异常!", e);
        }
    }

    /**
     * 设置属性值
     * @param i
     * @param operationLog
     * @param fieldName
     * @param value
     */
    private void setValue(int i, OperationLog operationLog, String fieldName, Object value) {
        if (value != null && !"".equals(value)) {
            if (i == 0) {
                operationLog.setBusinessNo(String.valueOf(value));
            } else {
                ReflectUtil.setFieldValue(operationLog, fieldName, value);
            }
        }
    }

}

日志表结构

屏幕快照 2022-12-22 下午10.03.12.png

Controller方法定义

在方法头加入@OpeLog注解,依次定义注解里面的属性,主要是对该方法的描述。


/**
 * 收款单导出
 *
 * @param
 */
@PostMapping("/export")
@ApiOperationSupport(order = 2)
@ApiOperation(value = "收款单-导出", notes = "传入receiptVoucherExportDTO对象")
@OpeLog(operationType = "收款单", operationItem = "导出", operationContent = "导出收款单", businessNoField = {"receiptVoucherNo"})
public void export(@Valid @RequestBody ReceiptVoucherExportDTO receiptVoucherExportDTO, HttpServletResponse response) {
	log.info("导出收款单 【入参】 export receiptVoucherExportDTO={}", receiptVoucherExportDTO);
	receiptVoucherService.export(receiptVoucherExportDTO, this.getCurrentUser(true), response);

}

调用这个导出方法后,看控制台输出,执行完方法的时候记录操作日志

屏幕快照 2022-12-22 下午10.00.18.png

总结

目前用这个切面记录操作日志的方式,还是比较常见的,通过mysql数据库进行操作记录的持久化,如果请求量比较大的情况下就不太支持这种方式了,还是建议使用ELK这样的日志搜索引擎,但是对于并发量不是很大的B端项目来说,这种记录操作日志的方式还是比较便捷的。