业务开发时经常需要打印出入参数据 方便出问题时定位问题,下面我给出两种实现方式,大家评判一下哪一个好
1、基于AOP实现
1.1 定义切面类
package com.cloud.infrastrure.common;import com.alibaba.fastjson.JSON;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;@Slf4j
@Aspect
@Component
@Order(Integer.MAX_VALUE)
public class LogAspect {private static final String LINE_SEPARATOR = System.lineSeparator();/*** 接口描述*/private String description = null;@Pointcut("within(com.cloud.interfaces.facade.*)")public void sportPoint() {// Do nothing because of X and Y.}@Pointcut("@annotation(org.springframework.web.bind.annotation.ExceptionHandler)")public void exceptionPointcut() {// Do nothing because of X and Y.}/*** 环绕*/@Around("within(com.cloud.interfaces.facade.*)")public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {long startTime = System.currentTimeMillis();// 开始打印请求日志ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();// 获取 @Log 注解的参数信息getAspectLogAttribute(proceedingJoinPoint);// 打印请求相关参数log.info("========================================== Start ==========================================");// 打印请求 urllog.info("URL : {}", request.getRequestURL().toString());// 打印描述信息log.info("Description : {}", description);// 打印 Http methodlog.info("HTTP Method : {}", request.getMethod());// 打印调用 controller 的全路径以及执行方法log.info("Class Method : {}.{}", proceedingJoinPoint.getSignature().getDeclaringTypeName(), proceedingJoinPoint.getSignature().getName());// 打印请求的 IPlog.info("IP : {}", request.getRemoteAddr());// 打印请求头
// log.info("Request Headers : {}", JSON.toJSONString(handlerHeaders(request)));// 打印请求入参log.info("Request Args : {}", handlerParameter(proceedingJoinPoint));// 检查appId、appKeyObject result = proceedingJoinPoint.proceed();// 打印出参log.info("Response Args : {}", JSON.toJSONString(result));// 执行耗时log.info("Time-Consuming : {} ms", System.currentTimeMillis() - startTime);// 接口结束后换行,方便分割查看log.info("=========================================== End ===========================================" + LINE_SEPARATOR);return result;}/*** 获取切面注解的描述** @param joinPoint 切点*/@SuppressWarnings("rawtypes")private void getAspectLogAttribute(JoinPoint joinPoint) throws ClassNotFoundException {String targetName = joinPoint.getTarget().getClass().getName();String methodName = joinPoint.getSignature().getName();Object[] arguments = joinPoint.getArgs();Class targetClass = Class.forName(targetName);Method[] methods = targetClass.getMethods();for (Method method : methods) {if (method.getName().equals(methodName)) {Class[] clazz = method.getParameterTypes();if (clazz.length == arguments.length) {if (method.getAnnotation(ApiOperation.class) != null) {description = method.getAnnotation(ApiOperation.class).value();}break;}}}}private Map<String, String> handlerHeaders(HttpServletRequest request) {Map<String, String> headerMap = new HashMap<>();Enumeration<String> headerNames = request.getHeaderNames();if (Objects.nonNull(headerNames)) {while (headerNames.hasMoreElements()) {String headerName = headerNames.nextElement();String headerValue = request.getHeader(headerName);headerMap.put(headerName, headerValue);}}return headerMap;}private String handlerParameter(JoinPoint point) {HashMap<String, Object> param = new HashMap<String, Object>(16);MethodSignature methodSignature = (MethodSignature) point.getSignature();String[] parameterNames = methodSignature.getParameterNames();Object[] args = point.getArgs();int i = 0;for (Object pojo : args) {//去掉空参数if (pojo == null || pojo instanceof HttpServletRequest|| pojo instanceof MultipartFile || pojo instanceof HttpServletResponse) {i++;continue;}param.put(parameterNames[i], pojo);i++;}String result = "";try {result = JSON.toJSONString(param);} catch (Exception e) {log.error("请求参数格式异常");}return result;}
}
编写一个Controller
package com.cloud.interfaces.facade;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;@Slf4j
@Validated
@RestController
@RequestMapping("/test")
@Api(tags = "Test API")
public class TestController {@ApiOperation(value = "测试order", notes = "测试order")@PostMapping("/Order")public Result<Order> clearTrade(@RequestBody Order order) {return Result.ofSucceed(order);}
打印的日志信息
2024-12-06 09:12:48,838--[TID: N/A]-- [http-nio-32122-exec-1] INFO com.haoran.cloud.pay.infrastrure.common.LogAspect - ========================================== Start ==========================================
2024-12-06 09:12:48,838--[TID: N/A]-- [http-nio-32122-exec-1] INFO com.cloud.infrastrure.common.LogAspect - URL : http://localhost:32122/error-card/query
2024-12-06 09:12:48,838--[TID: N/A]-- [http-nio-32122-exec-1] INFO com.cloud.infrastrure.common.LogAspect - Description : null
2024-12-06 09:12:48,838--[TID: N/A]-- [http-nio-32122-exec-1] INFO com.cloud.infrastrure.common.LogAspect - HTTP Method : POST
2024-12-06 09:12:48,838--[TID: N/A]-- [http-nio-32122-exec-1] INFO com.cloud.infrastrure.common.LogAspect - Class Method : com.cloud.interfaces.facade.TradeErrorCardController.query
2024-12-06 09:12:48,838--[TID: N/A]-- [http-nio-32122-exec-1] INFO com.haoran.cloud.infrastrure.common.LogAspect - IP : 127.0.0.1
2024-12-06 09:12:48,838--[TID: N/A]-- [http-nio-32122-exec-1] INFO com.cloud.infrastrure.common.LogAspect - Request Args : {"zoneId":"UTC"}
2、使用spring内置工具实现
打印入参使用 org.springframework.web.filter.CommonsRequestLoggingFilter
打印出参使用 OncePerRequestFilter 借助org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.CommonsRequestLoggingFilter;@Configuration
public class RequestLoggingConfig {@Beanpublic CommonsRequestLoggingFilter logFilter() {CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter();filter.setIncludeQueryString(true);filter.setIncludePayload(true);filter.setIncludeHeaders(true);filter.setIncludeClientInfo(true);filter.setAfterMessagePrefix("AfterMessagePrefix DATA ");filter.setBeforeMessagePrefix("BeforeMessagePrefix DATA ");return filter;}
}
当然如果log4j框架 是log42j.xml 文件配置需要增加
<Loggers><Logger additivity="true" name="org.springframework.web.filter.CommonsRequestLoggingFilter" level="debug"><AppenderRef ref="Console"/></Logger></Loggers>
如果是logback日志框架
logging:level:org.springframework.web.filter.CommonsRequestLoggingFilter: DEBUG
出参打印实现 基于 OncePerRequestFilter 和 ContentCachingResponseWrapper 结合
package com.cloud.infrastrure.config;import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.common.utils.IoUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;@Component
@Slf4j
public class ResponseWrapperFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);byte[] requestBody = requestWrapper.getContentAsByteArray();Map<String, String[]> parameterMap = requestWrapper.getParameterMap();log.info(JSON.toJSONString(parameterMap));String contentType = request.getContentType();log.info("request contentType " + contentType);
// ServletInputStream inputStream = requestWrapper.getInputStream();
// 这里不能获取 一旦获取 就不能再次读取了
// String toString = IoUtils.toString(inputStream, "UTF-8");log.info("requestBody1 " + new String(requestBody));
// log.info("requestBody2 " + toString);filterChain.doFilter(requestWrapper, responseWrapper);// 可以在这里处理响应数据byte[] responseBody = responseWrapper.getContentAsByteArray();log.info("responseBody " + new String(responseBody));// 处理body,例如添加签名responseWrapper.setHeader("X-Signature", "some-signature");// 必须调用此方法以将响应数据发送到客户端responseWrapper.copyBodyToResponse();}
}
3、回归总结
后续可以使用 springboot starter 来做
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.CommonsRequestLoggingFilter;import javax.annotation.PostConstruct;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;@Component
@Slf4j
@Order(2)
public class RequestLoggingFilter extends CommonsRequestLoggingFilter {@PostConstructpublic void init() {setIncludeQueryString(true);setIncludePayload(true);
// setIncludeHeaders(true);
// setIncludeClientInfo(true);setMaxPayloadLength(1024);}@Overrideprotected int getMaxPayloadLength() {return super.getMaxPayloadLength();}@Overrideprotected void afterRequest(HttpServletRequest request, String message) {if (request.getContentType().contains("application/json")) {log.info("client: {} url: {} requestBody {}", request.getRemoteAddr(), request.getRequestURL(), message);}}@Overrideprotected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {if (request.getRequestURI().startsWith("/actuator/health")) {return true;}if (request.getRequestURI().startsWith("/actuator/prometheus")) {return true;}return super.shouldNotFilter(request);}}
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingResponseWrapper;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;@Component
@Slf4j
@Order(1)
public class ResponseLoggingFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);filterChain.doFilter(request, responseWrapper);byte[] responseBody = responseWrapper.getContentAsByteArray();int contentSize = responseWrapper.getContentSize();if (responseWrapper.getContentType().contains("application/json")) {log.info("client: {} url: {} contentSize: {} responseBody {}", request.getRemoteAddr(), request.getRequestURL(), contentSize, new String(responseBody));}responseWrapper.copyBodyToResponse();}@Overrideprotected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {if (request.getRequestURI().startsWith("/actuator/health")) {return true;}if (request.getRequestURI().startsWith("/actuator/prometheus")) {return true;}return super.shouldNotFilter(request);}
}
4、日志内容压缩优化
对于那些出入参 contentSize 比较大的 比如一个列表查询结果 可能 有上万个字节
就需要对这些字节压缩 然后base64 encode 输出到日志 当我们遇到这类日志 就需要base64解码
具体优化后的类是
import com.common.CompressionUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingResponseWrapper;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Base64;@Component
@Slf4j
@Order(1)
public class ResponseLoggingFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);filterChain.doFilter(request, responseWrapper);byte[] responseBody = responseWrapper.getContentAsByteArray();int contentSize = responseWrapper.getContentSize();if (responseWrapper.getContentType().contains("application/json")) {if (contentSize > 1024) {byte[] compress = CompressionUtils.compress(responseBody);String message = Base64.getEncoder().encodeToString(compress);log.info("{} {} contentSize: {} compress {}", request.getMethod(), request.getRequestURI(), contentSize, message);} else {log.info("{} {} contentSize: {} {}", request.getMethod(), request.getRequestURI(), contentSize, new String(responseBody));}}responseWrapper.copyBodyToResponse();}@Overrideprotected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {if (request.getRequestURI().startsWith("/actuator/health")) {return true;}if (request.getRequestURI().startsWith("/actuator/prometheus")) {return true;}return super.shouldNotFilter(request);}
}
压缩工具是
import lombok.extern.slf4j.Slf4j;import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;
import java.util.zip.Deflater;
import java.util.zip.Inflater;@Slf4j
public class CompressionUtils {public static void main(String[] args) {compress("base64编码");}public static String compress(String encodeData) {String x = new String(decompress(Base64.getDecoder().decode(encodeData)));System.out.println(x);return x;}public static byte[] compress(byte[] data) {Deflater deflater = new Deflater();deflater.setInput(data);deflater.finish();byte[] buffer = new byte[1024];int compressedDataLength;try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(data.length)) {while (!deflater.finished()) {compressedDataLength = deflater.deflate(buffer);byteArrayOutputStream.write(buffer, 0, compressedDataLength);}return byteArrayOutputStream.toByteArray();} catch (IOException e) {log.error("compress failed", e);}return new byte[0];}public static byte[] decompress(byte[] data) {Inflater inflater = new Inflater();inflater.setInput(data);byte[] buffer = new byte[1024];int decompressedDataLength;try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(data.length)) {while (!inflater.finished()) {decompressedDataLength = inflater.inflate(buffer);byteArrayOutputStream.write(buffer, 0, decompressedDataLength);}return byteArrayOutputStream.toByteArray();} catch (IOException | java.util.zip.DataFormatException e) {log.error("decompress failed", e);}return new byte[0];}
}