Spring中AOP使用小结

在开发中经常需要对调用链路进行日志记录,如果只是简单的在调用方法中打印日志,显然无法达到效果。所以需要有一种更好的方式统一管理。而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 }
原文地址:https://www.cnblogs.com/gzhcsu/p/14339892.html