本文基于AOP提供一个后端收集日志的实现方案
1.依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
在项目中引入springboot的aop包后,可使用aop相关的功能
2.日志注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface OperateLog {
boolean enable() default true;
}
创建注解OperateLog ,该注解可设置在方法前,实现按需收集日志
基于大部分接口都会开启日志,enable默认值为true
基于给已有项目增加日志模块时,给每个接口都增加这个注解的话,改动过大,因此对没有添加该注解的接口,默认开启收集日志,即无需给每个接口加上该注解,只需给无需收集日志的接口加上注解并设置为false即可
扩展:如果需要对日志收集功能进行扩展,可在注解中配置更多属性,之后在收集日志的时候读取即可
比如下面注解扩展了explain属性
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface OperateLog {
boolean enable() default true;
String explain() default "";
}
除此之外,还可在接口方法上设置其他注解,在收集日志时获取其他注解的属性,也一样可以获取到更多信息
比如下面的接口上设置了ApiOperation注解,在日志收集时,即可直接读取ApiOperation的属性值,来获取该接口的名称
@ApiOperation("获取应用分页列表")
@GetMapping("/api/app/page")
public ResponseResult apiAppPage(@Validated ApiAppPageInVo inVo){
return applicationService.apiAppPage(this, inVo);
}
3.日志收集
@Aspect
@Slf4j
@Component
public class OperateLogAspect {
@Autowired
private LogDOMapper logDOMapper;
@Around("@annotation(io.swagger.annotations.ApiOperation)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//获取方法上的注解
ApiOperation apiOperation = getMethodAnnotation(joinPoint,ApiOperation.class);
OperateLog operateLog = getMethodAnnotation(joinPoint,OperateLog.class);
Date startTime = new Date();
try {
Object result = joinPoint.proceed();
//没有设置operateLog注解或者 operateLog属性值为true则收集日志,如果有其他条件,也可以继续扩展
if (operateLog==null||operateLog.enable()){
this.log(joinPoint, apiOperation, operateLog, startTime, result, null);
}
return result;
} catch (Throwable exception) {
if (operateLog==null||operateLog.enable()){
this.log(joinPoint, apiOperation, operateLog, startTime, null, exception);
}
throw exception;
}
}
private static <T extends Annotation> T getMethodAnnotation(ProceedingJoinPoint joinPoint, Class<T> annotationClass) {
return ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(annotationClass);
}
//result为接口返回值
private void log(ProceedingJoinPoint joinPoint, ApiOperation apiOperation, OperateLog operateLog,
Date startTime, Object result, Throwable exception) {
try {
LogDO logDO=new LogDO();
logDO.setApi(apiOperation.value());
//todo 这里生成日志,可加入其他数据,比如用户id,ip,入参等信息
logDOMapper.insert(logDO);
} catch (Throwable ex) {
log.error("[log][记录操作日志时,发生异常,其中参数是 joinPoint({}) operateLog({}) apiOperation({}) result({}) exception({}) ]",
joinPoint, operateLog, apiOperation, result, exception, ex);
}
}
}
上述代码实现了对拥有ApiOperation的注解的方法的增加操作
如果项目中没有使用ApiOperation作为接口的标志,也可以使用下面代码,对包名为com.xx.xxx.controller及其子包的所有方法进行增强,也可使用其他execution表达式更精确的执行
@Around("execution(* com.xx.xxx.controller..*.*(..))")
如果需要获取接口的入参和地址信息,可使用如下代码
//从HttpServletRequest 中获取地址
HttpServletRequest request = getRequest();
if (request!=null){
logDO.setUrl(request.getRequestURI());
}
//获取接口的入参属性
Object[] joinPointArgs = joinPoint.getArgs();
if (joinPointArgs != null) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
//获取接口的参数名称列表
String[] parameterNames = signature.getParameterNames();
Map<String, Object> parameterMap = new HashMap<>();
if (parameterNames != null) {
for (int i = 0; i < parameterNames.length; i++) {
//获取参数
Object parameterValue = joinPointArgs[i];
//某些类型的参数不需要记录,如果统计日志时报错了,可能是记录了无法序列化的参数,需要在这里增加
if (parameterValue != null && (parameterValue.getClass().isAssignableFrom(HttpServletRequest.class)
|| parameterValue.getClass().isAssignableFrom(HttpServletResponse.class)
|| parameterValue.getClass().isAssignableFrom(WebStatFilter.StatHttpServletResponseWrapper.class)
)) {
continue;
}
String parameterName = parameterNames[i];
//文件只记录文件名
if (parameterValue instanceof MultipartFile){
parameterMap.put(parameterName, ((MultipartFile) parameterValue).getOriginalFilename());
}
else{
parameterMap.put(parameterName, parameterValue);
}
}
}
logDO.setRequest(JSON.toJSONString(parameterMap));
}
getRequest方法代码为
public static HttpServletRequest getRequest() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (!(requestAttributes instanceof ServletRequestAttributes)) {
return null;
}
return ((ServletRequestAttributes) requestAttributes).getRequest();
}
4.优点
使用该方案收集日志,对现有项目入侵性小的同时,具有较高的扩展性