请求方式:GET
请求地址:http://localhost:8001/memo/taskLog/list
请求参数:{“taskId”:“47”}
请求耗时:17ms
HandlerInterceptor
preHandle
、afterCompletion
String reqParam = "";
if (Method.GET.toString().equalsIgnoreCase(method)) {
JSONObject jsonObject = new JSONObject();
Enumeration<String> parameterNames = request.getParameterNames();
if (parameterNames.hasMoreElements()) {
String paramName = parameterNames.nextElement();
String param = request.getParameter(paramName);
jsonObject.put(paramName, param);
}
reqParam = jsonObject.toString();
} else {
// 非 GET 方式都按照 POST 处理,且只处理 json 格式,因为导入时文件记录不了
if (StrUtil.isNotBlank(request.getContentType()) && request.getContentType().startsWith(MimeTypeUtils.APPLICATION_JSON_VALUE)) {
StringBuilder stringBuilder = new StringBuilder();
String str;
BufferedReader reader = request.getReader();
while ((str = reader.readLine()) != null) {
stringBuilder.append(str);
}
reqParam = JSON.parse(stringBuilder.toString()).toString();
}
}
因为在 HttpServletRequest
获取请求体body
参数的时候,是通过流的方式获取,就会多读取一次请求参数,而参数只允许读取一次,所以报错了
解决方案: 重写HttpServletRequest
为可重复读取的request
,写好之后又遇到问题,拦截器修改不了request
,所以只能改用过滤器
过滤器:实现Filter
在doFilter
方法重写HttpServletRequest
为可重复读取的request
,问题解决
正常场景记录日志没有问题,但是抛异常捕获不到,需要配合 @RestControllerAdvice
、@ExceptionHandler
在抛异常场景特殊处理,还要配合 ThreadLocal
传递异常信息
直接配置所有controller
的方法作为切面
通过环绕通知around
,能轻松获取请求参数,也不用重写HttpServletRequest
,也不用写一堆处理逻辑,异常也能直接捕获
请求方式和请求地址信息,通过 HttpServletRequest
获取
package com.gly.memo.common.aspect;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.ServletUtil;
import com.alibaba.fastjson2.JSON;
import com.gly.memo.common.exception.MyException;
import com.gly.memo.dto.vo.UserInfoVo;
import com.gly.memo.entity.SysLog;
import com.gly.memo.service.SysLogService;
import com.gly.memo.utils.AspectUtils;
import com.gly.memo.utils.ThreadLocalUtils;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.util.MimeTypeUtils;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.time.Duration;
import java.time.LocalDateTime;
/**
* 日志切面
*
* @author gaojie
* @date 2022-11-11
*/
@Slf4j
@Aspect
@Component
public class LogAspect {
@Resource
private HttpServletRequest request;
@Resource
private SysLogService sysLogService;
/**
* 使用环绕通知,拦截所有的 controller 请求
*/
@Around(value = "within(com.gly.memo.controller.*)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
UserInfoVo currentUser = ThreadLocalUtils.getCurrentUser();
SysLog sysLog = new SysLog();
// 1.日志基本参数
sysLog.setServerIp(InetAddress.getLocalHost().getHostAddress());
sysLog.setClientIp(ServletUtil.getClientIP(request));
sysLog.setUri(request.getRequestURI());
sysLog.setMethod(request.getMethod());
sysLog.setOperation(getOperation(joinPoint));
sysLog.setReqJson(getReqJson(joinPoint));
sysLog.setStartTime(LocalDateTime.now());
sysLog.setCreateBy(currentUser.getId());
sysLog.setCreateName(currentUser.getName());
try {
// 2.执行业务逻辑
Object result = joinPoint.proceed(joinPoint.getArgs());
// 3.1.请求成功后操作
sysLog.setSuccess(true);
sysLog.setResJson(JSON.toJSONString(result));
return result;
} catch (Exception e) {
log.info("异常信息: {}", e.getMessage());
// 3.2.抛异常后操作
sysLog.setSuccess(false);
sysLog.setException(e.getMessage());
sysLog.setBusinessException(e instanceof MyException);
throw e;
} finally {
// 3.3.任务计时
sysLog.setEndTime(LocalDateTime.now());
sysLog.setDuration((int) Duration.between(sysLog.getStartTime(), sysLog.getEndTime()).toMillis());
// 4.1.log方式打印
log.info("请求操作:{}", sysLog.getOperation());
log.info("请求方式:{}", sysLog.getMethod());
log.info("请求地址:{}", request.getRequestURL());
log.info("请求参数:{}", sysLog.getReqJson());
log.info("请求耗时:{}ms", sysLog.getDuration());
// 4.2.持久化
sysLogService.save(sysLog);
}
}
/**
* 获取请求参数 JSON
* 1.contentType 为空,表示为 GET 请求
* 2.contentType 前缀为 application/json,表示非 GET 请求,但是请求参数为 JSON 格式
* 3.否则有可能为导入的文件,请求过大而且格式为不可读,记录到日志无意义,所以不记录
*/
private String getReqJson(ProceedingJoinPoint joinPoint) {
String contentType = request.getContentType();
if (StrUtil.isBlank(contentType) || contentType.startsWith(MimeTypeUtils.APPLICATION_JSON_VALUE)) {
return JSON.toJSONString(joinPoint.getArgs());
} else {
return "非普通请求参数,暂不记录";
}
}
/**
* 获取操作类型
*/
private String getOperation(ProceedingJoinPoint joinPoint) {
ApiOperation annotation = AspectUtils.getAnnotation(joinPoint, ApiOperation.class);
return annotation.value();
}
}
缺点就是过滤器和拦截器处理的一段时间没有记录,不是特别精确
(不过核心功能已经实现,处理时间偏差几十ms
可以接受,如果想要时间最精确,实现ServletRequestListener
记录直接最准确,不过增加了系统复杂性)