springboot 日志記錄接口的請求參數(shù)和響應(yīng)結(jié)果的兩種方式-攔截器和切面(具體代碼)
前言:在生產(chǎn)中如果出現(xiàn)問題,我們想要查看日志,某個時間段用戶調(diào)用接口的請求參數(shù)和響應(yīng)的返回結(jié)果,通過日志來推測下用戶當(dāng)時做了什么操作。日志記錄接口的請求參數(shù)和響應(yīng)結(jié)果有利于我們排查生產(chǎn)的問題,但是也會給系統(tǒng)帶來內(nèi)存性能的問題。所以我們需要權(quán)衡其中的利弊來選擇,下面就是記錄日志兩種方式的具體代碼。
一、使用切面(推薦使用這種,簡單)
@Component
@Aspect
@Slf4j
public class ApiLogAspect {
@Pointcut("execution(* com.xl.finance.module..controller..*.*(..))")
public void controller() {
}
@Around("controller()")
public Object around(ProceedingJoinPoint point) throws Throwable {
long startTime = System.currentTimeMillis();
Long userId = WebUtils.getId();
String userName = WebUtils.getName();
String requestUrl = WebUtils.getRequestUrl();
requestUrl = new URL(requestUrl).getPath();
String requestParam = getMethodArgs(point);
Object result = point.proceed();
long endTime = System.currentTimeMillis();
String responseParam = handlerResult(result);
log.info("requestUrl:{} userId:{},userName:{} requestParam:{},responseParam:{},rtt:{}ms", requestUrl, userId, userName, requestParam, responseParam, endTime - startTime);
return result;
}
private String getMethodArgs(JoinPoint point) {
Object[] args = point.getArgs();
if (args == null || args.length == 0) {
return "";
}
try {
Map<String, Object> params = new HashMap<>();
String[] parameterNames = ((MethodSignature) point.getSignature()).getParameterNames();
for (int i = 0; i < parameterNames.length; i++) {
Object arg = args[i];
// 過濾不能轉(zhuǎn)換成JSON的參數(shù)
if ((arg instanceof ServletRequest) || (arg instanceof ServletResponse)) {
continue;
} else if ((arg instanceof MultipartFile)) {
arg = arg.toString();
}
params.put(parameterNames[i], arg);
}
return JSONObject.toJSONString(params);
} catch (Exception e) {
log.error("接口出入?yún)⑷罩敬蛴∏忻嫣幚碚埱髤?shù)異常", e);
}
return Arrays.toString(args);
}
/**
* 返回結(jié)果簡單處理
* 1)把返回結(jié)果轉(zhuǎn)成String,方便輸出。
* 2)返回結(jié)果太長則截取(最多3072個字符),方便展示。
*
* @param result 原方法調(diào)用的返回結(jié)果
* @return 處理后的
*/
private String handlerResult(Object result) {
if (result == null) {
return null;
}
String resultStr;
try {
if (result instanceof String) {
resultStr = (String) result;
} else {
resultStr = JSONObject.toJSONString(result);// 如果返回結(jié)果非String類型,轉(zhuǎn)換成JSON格式的字符串
}
if (resultStr.length() > 3072) {
resultStr = resultStr.substring(0, 3072);
}
} catch (Exception e) {
resultStr = result.toString();
log.error("接口出入?yún)⑷罩敬蛴∏忻嫣幚矸祷貐?shù)異常", e);
}
return resultStr;
}
}
二、使用攔截器
本來以為這種方式只要自定義攔截器繼承HandlerInterceptorAdapter類重寫preHandle() 和 afterCompletion()就可以了。沒想到還是遇到挺多坑。
請求參數(shù):request.getParameterMap()可以獲取到請求參數(shù),但是如果接口是使用@RequestBody,就會發(fā)現(xiàn)得不到值。
響應(yīng)結(jié)果:這是比較蛋疼一點(diǎn),我?guī)缀醪榱藃esponse的所有方法,都發(fā)現(xiàn)沒法得到接口響應(yīng)結(jié)果。
下面直接貼代碼。
2.1 實(shí)現(xiàn)Filter類
流是一次性的,為了防止在日志中讀取到流的請求參數(shù),影響實(shí)際請求的接口文章來源:http://www.zghlxwxcb.cn/news/detail-785741.html
@Component
@WebFilter(filterName = "HttpServletRequestFilter", urlPatterns = "/")
@Order(10000)
public class HttpServletRequestFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
ServletRequest requestWrapper = null;
if(servletRequest instanceof HttpServletRequest) {
requestWrapper = new RequestWrapper((HttpServletRequest) servletRequest);
}
//獲取請求中的流如何,將取出來的字符串,再次轉(zhuǎn)換成流,然后把它放入到新request對象中
// 在chain.doFiler方法中傳遞新的request對象
if(null == requestWrapper) {
filterChain.doFilter(servletRequest, servletResponse);
} else {
filterChain.doFilter(requestWrapper, servletResponse);
}
}
@Override
public void destroy() {
Filter.super.destroy();
}
}
2.2 繼承RequestWrapper類
@Slf4j
public class RequestWrapper extends HttpServletRequestWrapper {
private final String body;
public RequestWrapper(HttpServletRequest request) {
super(request);
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader = null;
try (InputStream inputStream = request.getInputStream()) {
if(inputStream != null){
bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
char[] charBuffer = new char[128];
int bytesRead = -1;
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
stringBuilder.append(charBuffer, 0, bytesRead);
}
}
} catch (Exception e) {
log.error("RequestWrapper read error :{}",e.getMessage());
} finally {
IoUtil.close(bufferedReader);
}
body = stringBuilder.toString();
}
/**
* 將getInputStream重新,讓它能重復(fù)獲取到body里的內(nèi)容,這樣才不會影響后續(xù)的流程
* @return
* @throws IOException
*/
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
ServletInputStream servletInputStream = new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() throws IOException {
return byteArrayInputStream.read();
}
};
return servletInputStream;
}
/**
* 重寫獲取 字符流的方式
* @return
*/
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream(), StandardCharsets.UTF_8));
}
/**
* 獲取body
* @return
*/
public String getBody() {
return this.body;
}
}
2.3 實(shí)現(xiàn)ResponseBodyAdvice類
在實(shí)現(xiàn)ResponseBodyAdvice,就可以獲取到接口響應(yīng)的結(jié)果來打印日志,但是為了統(tǒng)一在自定義的攔截器處理日志,這里把響應(yīng)的結(jié)果存在response的一個屬性里。文章來源地址http://www.zghlxwxcb.cn/news/detail-785741.html
@ControllerAdvice
public class InterceptResponse implements ResponseBodyAdvice<Object> {
private static final Integer MAX_LENGTH = 1000;
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
String result ;
if(body instanceof JsonResult){
JsonResult jsonResult = (JsonResult) body;
result = JSON.toJSONString(jsonResult);
}else{
result = JSON.toJSONString(body);
}
Integer length = result.length() > MAX_LENGTH ? MAX_LENGTH : result.length();
result = result.substring(0,length);
HttpServletRequest httpServletRequest = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
HttpSession httpSession = httpServletRequest.getSession(true);
//放到緩存里,以便于可以在HandlerInterceptor攔截里取出并打印出返回結(jié)果
httpSession.setAttribute("body", result);
return body;
}
}
2.4 新建定義攔截器繼承HandlerInterceptorAdapter
@Component
@Slf4j
public class LogMonitorInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestParams = StringUtils.EMPTY;
if (request instanceof RequestWrapper) {
requestParams = ((RequestWrapper) request).getBody();
}
if(StrUtil.isEmpty(requestParams)){
requestParams = JSON.toJSONString(request.getParameterMap());
}
log.info("request: uri:{} , type:{} , ip:{}, operatorId:{}, operatorName:{}, params:{}",
request.getRequestURI(),request.getMethod(),request.getRemoteAddr(),
WebUtils.getId(),WebUtils.getUsername(), requestParams );
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HttpSession httpSession = request.getSession();
String result = (String) httpSession.getAttribute("body");
log.info("response: url:{} , type:{} , ip:{}, operatorId:{}, operatorName:{}, result:{}",
request.getRequestURI(),request.getMethod(),request.getRemoteAddr(),
WebUtils.getId(),WebUtils.getUsername(), result );
}
}
2.5 實(shí)現(xiàn)WebMvcConfigurer
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private LogMonitorInterceptor logMonitorInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 接口操作日志攔截器
registry.addInterceptor(logMonitorInterceptor);
}
}
到了這里,關(guān)于springboot 日志記錄接口的請求參數(shù)和響應(yīng)結(jié)果的兩種方式-攔截器和切面(具體代碼)的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!