AOP记录请求日志

831 阅读2分钟

前言

使用AOP处理请求日志,记录请求ipurl、请求参数、请求结果等信息,并异步保存日志信息方便对线上问题进行排查分析。

思路

  • @RequestMapping@GetMapping等注解切入,@Around环绕通知处理日志信息

  • 使用Spring事件发布订阅与@Async保存日志信息

实现

切面

  • 日志信息定义

    @Data
    public class RequestLog implements Serializable {
        // id
        private String id;
        // 请求url
        private String url;
        // 请求方法
        private String method;
        // 客户端ip
        private String ip;
        // 用户代理
        private String userAgent;
        // 请求调用类(哪个controller)
        private String module;
        // 请求调用类方法(@RequestMapping等注解的方法名)
        private String operation;
        // 请求参数
        private String args;
        // 请求结果
        private Object result;
        // 是否成功(发生Exception为false)
        private boolean success;
        // 执行时长(单位:ms)
        private long duration;
        // 创建人
        private String createdBy;
        // 创建时间
        private LocalDateTime createdAt;
    }
    
  • 切面定义

    @Slf4j
    @Aspect
    @Component
    public class RequestLogAspect {
        @Autowired
        private ApplicationEventPublisher publisher;
    
        @Autowired
        private ObjectMapper objectMapper;
        
        // 异常信息最大长度
        private static final int MAX_EXCEPTION_LEN = 2048;
    
         @Pointcut(
                "@annotation(org.springframework.web.bind.annotation.RequestMapping) ||" +
                        "@annotation(org.springframework.web.bind.annotation.GetMapping) || " +
                        "@annotation(org.springframework.web.bind.annotation.PostMapping) ||" +
                        "@annotation(org.springframework.web.bind.annotation.PutMapping) || " +
                        "@annotation(org.springframework.web.bind.annotation.DeleteMapping) ")
        public void pointCut() {
        }
    
        @Around("pointCut()")
        public Object logRequest(ProceedingJoinPoint joinPoint) throws Throwable {
            StopWatch stopWatch = new StopWatch();
            stopWatch.start(UUID.randomUUID().toString());
    
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            Method method = methodSignature.getMethod();
            RequestLog requestLog = initLog();
            requestLog.setModule(method.getDeclaringClass().getName());
            requestLog.setOperation(method.getName());
            requestLog.setArgs(getArgs(joinPoint));
            Object result = null;
    
            try {
                result = joinPoint.proceed();
                return result;
            } catch (Exception exception) {
                requestLog.setSuccess(false);
                String exceptionStr = exception.toString();
                result = exceptionStr.length() > MAX_EXCEPTION_LEN ? exceptionStr.substring(0, MAX_EXCEPTION_LEN) : exceptionStr;
                throw exception;
            } finally {
                stopWatch.stop();
                requestLog.setDuration(stopWatch.getTotalTimeMillis());
                requestLog.setResult(result);
                publisher.publishEvent(new RequestLogEvent(requestLog));
            }
        }
    
        private String getArgs(ProceedingJoinPoint joinPoint) {
            Object[] args = joinPoint.getArgs();
            String[] names = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
            Map<String, Object> argMap = new HashMap<>();
    
            if (Objects.nonNull(args) && args.length > 0) {
                for (int i = 0, len = args.length; i < len; i++) {
                    Object arg = args[i];
                    // 请求响应等参数不处理
                    if (arg instanceof ModelAndView || arg instanceof HttpServletRequest
                            || arg instanceof HttpServletResponse || (arg instanceof MultipartFile)
                            || (arg instanceof MultipartFile[])) {
                        continue;
                    }
                    argMap.put(names[i], arg);
                }
            }
    
            String argStr = "";
            try {
                if (argMap.size() > 0) {
                    argStr = objectMapper.writeValueAsString(argMap);
                }
            } catch (Exception ex) {
            }
            return argStr;
        }
    
        private RequestLog initLog() {
            RequestLog requestLog = new RequestLog();
            requestLog.setId(UUID.randomUUID().toString());
            requestLog.setCreatedAt(LocalDateTime.now());
            requestLog.setSuccess(true);
    
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            requestLog.setUserAgent(request.getHeader("user-agent"));
            requestLog.setIp(getIp(request));
            requestLog.setUrl(request.getRequestURI());
            requestLog.setMethod(request.getMethod());
    
            // log.setCreatedBy(); TODO: 从上下文获取用户id
    
            return requestLog;
        }
    
        private String getIp(HttpServletRequest request) {
            String ipAddress = null;
            try {
                ipAddress = request.getHeader("x-forwarded-for");
                if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                    ipAddress = request.getHeader("Proxy-Client-IP");
                }
                if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                    ipAddress = request.getHeader("WL-Proxy-Client-IP");
                }
                if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                    ipAddress = request.getRemoteAddr();
                    if (Objects.equals("127.0.0.1", ipAddress)) {
                        // 根据网卡取本机配置的IP
                        InetAddress inet = null;
                        try {
                            inet = InetAddress.getLocalHost();
                        } catch (UnknownHostException e) {
                            e.printStackTrace();
                        }
                        ipAddress = inet.getHostAddress();
                    }
                }
                // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
                if (ipAddress != null && ipAddress.length() > 15) {
                    // = 15
                    if (ipAddress.indexOf(",") > 0) {
                        ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
                    }
                }
            } catch (Exception e) {
                ipAddress = "";
            }
            return ipAddress;
        }
    }
    
    

事件订阅

  • 事件定义

    @AllArgsConstructor
    @Data
    public class RequestLogEvent implements Serializable {
    
        private RequestLog requestLog;
    }
    
  • 事件订阅

    @Slf4j
    @Component
    public class RequestLogListener {
    
        // 使用 @Async 需先启用 @EnableAsync 注解
        @Async
        @EventListener(RequestLogEvent.class)
        public void logRequest(RequestLogEvent event) {
            RequestLog requestLog = event.getRequestLog();
            // TODO: 可发送到消息队列、保存到数据库等
            log.info("requestLog {}", requestLog);
        }
    }
    

结尾

如果文章对你有帮助,请点 赞👍🏻 支持一下。若有错误之处或有更好的建议,欢迎指正,谢谢。