浅谈操作日志

362 阅读3分钟

1、使用场景

记录操作日志,是大部分的系统都会有的一项功能。那么怎么记录,能做到接入简单,又不会与业务代码过度耦合呢?

2、实现方式

2.1、在业务代码后面直接生成记录

public Result<Void> create(SceneCreateRequest request) {
    sceneCreateCommand.execute(request);
    oplogAdapter.log(request.getOrgId(), request.getMemberId(), OplogEnum.create_scene,request.getIp(),null);
    return Result.success();
}

这种方式优点在于,可以准确快速获得上下文参数,详细记录各种业务执行情况。但是缺点也很明显,辅助代码和业务代码耦合。上面的示例还比较简单,一旦业务逻辑变得复杂之后,不仅严重影响到可读性、可维护性,还会影响到业务代码执行时间。

2.2、注解+aop

为了解决上面出现的问题,一般会使用注解去拦截需要记录日志的方法。

首先先定义一个注解,可以使用枚举来表示有哪些行为。也可以使用动态模版来动态生成日志内容。

//@Oplog(action = "#action")
@Oplog(actionEnum = UserActionEnum.CREATE_MONITOR)
@PostMapping("/monitor")
public Result create(@RequestBody MonitorCreateRequest request) {
    return monitorService.create(request);
}

但是使用注解会带来几个问题。

1、比如,如果一个方法代表多种行为,那么这种方法就不太好记录了。

//资源有多种类型,无法根据资源id区分 

@DeleteMapping("/xxx/xxx/xxx/xxx/{businessResourceId}")
public Result<Void> delete(@PathVariable("businessResourceId") Long businessResourceId){
    return resourceService.delete(businessResourceId);
}

可以在aop里去根据id查询资源所对应的类型,然后再进行记录。但是这种处理方法就会导致aop类不通用,会有很多特殊判断、处理。

2、使用动态模版这种方式也会带来几个问题。

比如:如果需要在注解里使用其他变量,那么怎么去拿到这个变量的值?

① 可以把这个参数放到操作日志的线程上下文中,然后注解上的模版就能使用到了。

但是这样也会在业务代码之间插入几条无关业务逻辑的代码。

@Oplog(action = "#action")
public Result create(@RequestBody MonitorCreateRequest request) {
    OplogContext.putVariable("action",xxxx.getAction());
    return monitorService.create(request);
}

② 也可以直接在aop中去拿到想要的变量

但是这样处理起来很麻烦,也不够通用、优雅

解决办法:

可以使用一个自定义函数,把request请求里的信息传递到函数里,再计算拿到想要的值。

@Oplog( action = "#{T(com.xxx.xxx.xxx.domain.resource.Resource).getActionByFunctionType(#request.functionType)}",
        functionType = "#request.functionType")
public Result<ResourceUploadResponse> upload(@RequestBody ResourceUploadRequest request){
    return resourceService.upload(request);
}

public static String getActionByFunctionType(Integer function){
    if(function.equals(FunctionEnum.COMMAND_LINE.getFunctionType())){
        return UserActionEnum.SCRIPT_UPLOAD.getAction();
    }else if(function.equals(FunctionEnum.PUSH_WALLPAPER.getFunctionType())){
        return UserActionEnum.WALLPAPER_UPLOAD.getAction();
    }else {
        return UserActionEnum.FILE_UPLOAD.getAction();
    }
}

3、实现代码

我们采用的是注解+枚举的方式,不过随着业务的迭代,也在进行设计调整。这里只展示注解+枚举的代码实现了。

3.1 首先定义一个名为@Oplog的注解。

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

/**
 * 行为名称
 * (用来兼容一些无法直接拿到actionEnum的接口)
 * 如果这个为默认值,则会使用 actionEnum
 */
String action() default "";

/**
 * 行为枚举
 */
UserActionEnum actionEnum() default UserActionEnum.OTHER_COMMAND;

/**
 * 模块 
 */
int module() default 0;

/**
 * 功能类型 
 */
String functionType() default "-1";
}

3.2 在aop里配置SpEL解析器、解析模版、类路径和注解的路径。

public class OplogAspect implements BeanFactoryAware{
    private static final TemplateParserContext PARSER_CONTEXT = new TemplateParserContext();

    private static final SpelExpressionParser PARSER = new SpelExpressionParser();

    private final StandardEvaluationContext evaluationContext = new StandardEvaluationContext();

    private BeanFactory beanFactory;

    @Override
    public void setBeanFactory(@NotNull BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
        this.evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory));
    }
    
    @Pointcut("execution(* com.xxx.xxx.xxx.xxx.xxx.admin..*.*(..)) && @annotation(com.xxx.xxx.xxx.xxx.annotation.Oplog)")
    public void pointCut() {
    }  
}

3.3 拿到请求参数

Method targetMethod = this.getTargetMethod(joinPoint);
Oplog oplog = targetMethod.getAnnotation(Oplog.class);

//参数解析
LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
String[] params = discoverer.getParameterNames(targetMethod);
Object[] args = joinPoint.getArgs();

for (int len = 0; len < Objects.requireNonNull(params).length; len++) {
    evaluationContext.setVariable(params[len], args[len]);
}

3.4 解析参数

//如果没有使用action属性,那就使用actionEnum属性,如果都是默认值,那就不记录
String action;
Integer actionType;
assert StrUtil.isBlank(oplog.action()) && UserActionEnum.OTHER_COMMAND.equals(oplog.actionEnum());
if(StrUtil.isNotBlank(oplog.action())){
    action = PARSER.parseExpression(resolve(oplog.action()),PARSER_CONTEXT).getValue(evaluationContext,String.class);
}else {
    action = oplog.actionEnum().getAction();
}

Integer functionType = PARSER.parseExpression(resolve(oplog.functionType())).getValue(evaluationContext,Integer.class);

private String resolve(String value) {
    if (this.beanFactory != null && this.beanFactory instanceof ConfigurableBeanFactory) {
        return ((ConfigurableBeanFactory) this.beanFactory).resolveEmbeddedValue(value);
    }
    return value;
}

private Method getTargetMethod(JoinPoint joinPoint) throws NoSuchMethodException {
    Signature signature = joinPoint.getSignature();
    MethodSignature methodSignature = (MethodSignature)signature;
    Method agentMethod = methodSignature.getMethod();
    return joinPoint.getTarget().getClass().getMethod(agentMethod.getName(),agentMethod.getParameterTypes());
}

3.5 落库

ActionRecord actionRecord = new ActionRecord()
.setOrgCode(SecurityUtils.getOrgCode())
.setAction(action)
.setFunctionType(functionType)
.......
.create();