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();