在开发中经常需要对调用链路进行日志记录,如果只是简单的在调用方法中打印日志,显然无法达到效果。所以需要有一种更好的方式统一管理。而Spring中提供的aop就可以很好的解决此问题。
参考学习网站:https://www.cnblogs.com/bigben0123/p/7779357.html
需求:① 调用链路 ② 日志统一整合
/** * @author betterLearning */ @Aspect @Component public class LogAspect { private static final Logger LOGGER = LoggerFactory.getLogger(LogAspect.class); /** 定义被切包及其子包内的方法 */ @Pointcut(value = "execution(* com.it.betterlearn.controller..*.*(..))") public void controllerPointcut(){} @Around("controllerPointcut()") public Object around(ProceedingJoinPoint pjp) throws Throwable { // 接收请求 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); // 记录请求内容 LOGGER.info("URL:{}",request.getRequestURL().toString()); LOGGER.info("IP:{}",request.getRemoteAddr()); String classMethod = pjp.getSignature().getDeclaringTypeName() + "." + pjp.getSignature().getName(); LOGGER.info("CLASS_METHOD:{}", classMethod); // 记录请求入参 Object[] args = pjp.getArgs(); if (args.length > 0) { StringBuilder sb = new StringBuilder(); for (Object arg : args) { sb.append(JSON.toJSONString(arg)); } LOGGER.info("classMethod:{}|params:{}", classMethod, sb.toString()); } // 获取调用链路号(全局唯一),约定俗称key:traceId。value:全局唯一号 String traceId = MDC.get("traceId"); // 如果没有,自身产生一个。比如当前服务属于调用链路中的第一个。 if (traceId == null) { MDC.put("traceId", UUID.randomUUID().toString().replace("-", "")); } Exception exception = null; Object result = null; long begin = System.currentTimeMillis(); try { return result = pjp.proceed(); } catch (Exception ex) { exception = ex; throw ex; } finally { long spendTime = System.currentTimeMillis()-begin; if (exception != null) { LOGGER.info("classMethod:{}|spendTime:{}|exception:{}", classMethod, spendTime, JSON.toJSONString(exception)); } else { LOGGER.info("classMethod:{}|spendTime:{}|result:{}", classMethod, spendTime, JSON.toJSONString(result)); } // 从MDC删除调用链路号 if (traceId != null) { MDC.remove(traceId); } MDC.clear(); } } }
最后还需要在日志格式中添加traceId的打印:
<property name="pattern">[TRACEID:%X{traceId}] %d{HH:mm:ss.SSS} %-5level %class{-1}.%M()/%L - %msg%xEx%n</property>
基于上面的方式,代码看起来还是有点凌乱,而且打印的内容没有做封装,不好管理和迭代,甚至在高并发情况下,日志出现串行(虽然有链路号,可以判断),所以下面继续优化。
封装正常请求信息头和错误信息头
/** * @author betterLearning */ @Data public class RequestInfo { private String ip; private String url; private String httpMethod; private String classMethod; private Object requestParams; private Object result; private Long timeCost; }
/** * @author betterLearning */ @Data public class RequestErrorInfo { private String ip; private String url; private String httpMethod; private String classMethod; private Object requestParams; private RuntimeException exception; }
切面代码,注:此处没有把traceId加上。
1 /** 2 * @author betterLearning 3 */ 4 @Aspect 5 @Component 6 public class LogAspect { 7 private final static Logger LOGGER = LoggerFactory.getLogger(LogAspect.class); 8 9 @Pointcut("execution(* com.it.demo.controller..*(..))") 10 public void controllerPointcut(){}; 11 12 /** 13 * 正常通知环绕体 14 * @param proceedingJoinPoint 15 * @return 16 * @throws Throwable 17 */ 18 @Around("controllerPointcut()") 19 public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { 20 long start = System.currentTimeMillis(); 21 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); 22 HttpServletRequest request = attributes.getRequest(); 23 // 封装请求 24 RequestInfo requestInfo = new RequestInfo(); 25 requestInfo.setIp(request.getRemoteAddr()); 26 requestInfo.setUrl(request.getRequestURL().toString()); 27 requestInfo.setHttpMethod(request.getMethod()); 28 requestInfo.setClassMethod(String.format("%s.%s", proceedingJoinPoint.getSignature().getDeclaringTypeName(), 29 proceedingJoinPoint.getSignature().getName())); 30 requestInfo.setRequestParams(getRequestParamsByProceedingJoinPoint(proceedingJoinPoint)); 31 // 执行目标方法 32 Object result = proceedingJoinPoint.proceed(); 33 // 封装执行结果 34 requestInfo.setResult(result); 35 // 答应请求耗时 36 requestInfo.setTimeCost(System.currentTimeMillis() - start); 37 LOGGER.info("Request Info: {}", JSON.toJSONString(requestInfo)); 38 return result; 39 } 40 41 /** 42 * 异常通知。区别于正常环绕,不打印耗时; 43 * @param joinPoint 44 * @param e 45 */ 46 @AfterThrowing(pointcut = "controllerPointcut()", throwing = "e") 47 public void doAfterThrow(JoinPoint joinPoint, RuntimeException e) { 48 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); 49 HttpServletRequest request = attributes.getRequest(); 50 RequestErrorInfo requestErrorInfo = new RequestErrorInfo(); 51 requestErrorInfo.setIp(request.getRemoteAddr()); 52 requestErrorInfo.setUrl(request.getRequestURL().toString()); 53 requestErrorInfo.setHttpMethod(request.getMethod()); 54 requestErrorInfo.setClassMethod(String.format("%s.%s", joinPoint.getSignature().getDeclaringTypeName(), 55 joinPoint.getSignature().getName())); 56 requestErrorInfo.setRequestParams(getRequestParamsByJoinPoint(joinPoint)); 57 requestErrorInfo.setException(e); 58 LOGGER.info("Error Request Info: {}", JSON.toJSONString(requestErrorInfo)); 59 } 60 /** 61 * 获取请求参数。注:对于@PathVariable以及@RequestParam注解传递的参数无法打印出参数名 62 * @param proceedingJoinPoint 63 * @return 64 */ 65 private Map<String, Object> getRequestParamsByProceedingJoinPoint(ProceedingJoinPoint proceedingJoinPoint) { 66 //参数名 67 String[] paramNames = ((MethodSignature)proceedingJoinPoint.getSignature()).getParameterNames(); 68 //参数值 69 Object[] paramValues = proceedingJoinPoint.getArgs(); 70 return buildRequestParam(paramNames, paramValues); 71 } 72 73 private Map<String, Object> getRequestParamsByJoinPoint(JoinPoint joinPoint) { 74 //参数名 75 String[] paramNames = ((MethodSignature)joinPoint.getSignature()).getParameterNames(); 76 //参数值 77 Object[] paramValues = joinPoint.getArgs(); 78 return buildRequestParam(paramNames, paramValues); 79 } 80 81 /** 82 * 封装参数名和参数值,一一对应。注:此处对于文件对象做了特殊处理 83 * @param paramNames 84 * @param paramValues 85 * @return 86 */ 87 private Map<String, Object> buildRequestParam(String[] paramNames, Object[] paramValues) { 88 Map<String, Object> requestParams = new HashMap<>(16); 89 for (int i = 0; i < paramNames.length; i++) { 90 Object value = paramValues[i]; 91 //如果是文件对象 92 if (value instanceof MultipartFile) { 93 MultipartFile file = (MultipartFile) value; 94 //获取文件名 95 value = file.getOriginalFilename(); 96 } 97 requestParams.put(paramNames[i], value); 98 } 99 return requestParams; 100 } 101 }