最近业务有需求要对所有的用户操作进行日志记录,方便管理员查询不同权限级别的用户对系统的操作记录,现有的日志只是记录了异常信息、业务出错、重要功能的执行进行了记录,并不能满足需求要求,最直接的解决方法是在每个接口上去添加log.info之类的代码,但是这种方式对业务代码的植入性太强,记录日志的代码和业务代码耦合性太强,对于代码的可读性和可维护性来说是一个灾难。如果能在每个接口上添加一个注解来记录日志则要优雅的多。 通过接口来记录日志需要用到Spring的动态代理技术,定义一个切面,应用到所有加了这个注解的接口,在接口执行的切面上获取接口方法的参数和执行结果,将要记录的信息记录到数据库(或者是日志文件或者是其他方式)。
1、定义注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface LogBook {
/**
* 模块
*
* @return
*/
String module();
/**
* 跟踪标识(业务标识)
*
* @return
*/
String traceId();
/**
* 跟踪标签(业务标识标签)
*
* @return
*/
String traceTag() default "";
/**
* 操作内容
*
* @return
*/
String[] content();
/**
* 操作类型
* 为null时,框架根据request-method进行匹配
*
* @return
*/
String operateType() default "";
/**
* 操作员
*
* @return
*/
String operator() default "_header";
/**
* 启停
*
* @return
*/
boolean enable() default true;
}
2、定义切面类:
定义切面类使用Aop的注解@Aspect来定义,对制定的注解进行环切,定义如下:
@Aspect
public class GenericRestLogBookAspect {
//切面 包含注解
@Pointcut("@annotation(com.xxx.annotation.LogBook)")
public void intercept() {
}
/**
* 对方法进行环切
*
* @param joinPoint
* @return
* @throws Throwable
*/
@Around(value = "intercept()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
/**
* 当前注解实例
*/
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
LogBook annotation = AnnotationUtils.findAnnotation(methodSignature.getMethod(), LogBook.class);
if (annotation == null || !annotation.enable()) {
return joinPoint.proceed();
}
/**
* 基础参数
*/
Object[] args = joinPoint.getArgs();
HttpServletRequest request = this.getRequest(args);
LogBookRecord init = new LogBookRecord();
init.setCreateDate(new Date()); //标准时间
EvaluationContext evaluationContext = expressionParser.initContext(joinPoint);
/**
* 执行
*/
Object out = null;
Exception err = null;
try {
//前置
this.beforeResolving(request, evaluationContext, annotation, args, init);
//业务执行
out = joinPoint.proceed();
} catch (Exception e) {
err = e;
throw e;
} finally {
//后置
this.afterResolving(request, evaluationContext, annotation, args, init, out, err);
//释放
expressionParser.removeContext();
}
return out;
}
/**
* 前置处理
*
* @param handler
* @param request
* @param evaluationContext
* @param annotation
* @param args
* @param init
*/
private void beforeResolving(HttpServletRequest request,
EvaluationContext evaluationContext,
LogBook annotation,
Object[] args,
LogBookRecord init) {
//这里的resolving方法从request中解析出相关参数信息到LogBookRecord对象中
resolving(request, evaluationContext, annotation, args, init);
}
/**
* 后置处理
*
* @param handler
* @param request
* @param evaluationContext
* @param annotation
* @param args
* @param record
* @param result
* @param err
*/
private void afterResolving(HttpServletRequest request,
EvaluationContext evaluationContext,
LogBook annotation,
Object[] args,
LogBookRecord record,
Object result, Exception err) {
/**
* 异步处理
*/
try {
//重新构建 EvaluationContext
EvaluationContext evaluationAfter = expressionParser.setContextResult(evaluationContext, result, err);
threadPool.getTaskExecutor().execute(() -> {
//解析 - 根据业务接口返回结果信息解析到LogBookRecord中
List<LogBookRecord> records = resolvingAfter(request, evaluationAfter, annotation, args, result, err, record);
//存储(可以存储到数据库,也可以存储到日志文件或者其他地方)
persistHandler.saveBatch(records);
});
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
/**
* 默认从参数中获取
*
* @param args
* @return
*/
private HttpServletRequest getRequest(Object[] args) {
Optional<HttpServletRequest> ops = Stream.of(args).filter(e -> e.getClass().isAssignableFrom(HttpServletRequest.class)).map(e -> (HttpServletRequest) e).findFirst();
return ops.orElseGet(() -> ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest());
}
}
3、定义开关属性:
通过这个开关属性,控制开启还是关闭日志记录。
@Configuration
@ConfigurationProperties(prefix = "xxx.logbook")
public class LogBookProperties implements Serializable {
private boolean enable;
public boolean isEnable() {
return enable;
}
public void setEnable(boolean enable) {
this.enable = enable;
}
}
4、定义配置类:
在配置类中定义切面类的Bean,并通过开关属性进行开关控制;
@Configuration
@ConditionalOnProperty(name = "xxx.logbook.enable", havingValue = "true")
@ComponentScan("com.xxx.logbook")
public class LogBookAutoConfiguration {
/**
* 切面
*
* @param factory
* @param expressionParser
* @return
*/
@Bean
public GenericRestLogBookAspect logBookAspect(LogBookExpressionParser expressionParser,
LogBookPersistHandler persistHandler, LogBookThreadPool threadPool) {
return new GenericRestLogBookAspect(expressionParser, persistHandler, threadPool);
}
}
5、应用示例:
@LogBook(module = "custom",
traceId = "{{#dto.id}}",
content = {"用户 :提交了新数据:{{#dto.filed1}} {{#dto.filed2}} {{#dto.filed3}} "},
operateType = OperateTypes.ADD)