springboot 日志记录接口的请求参数和响应结果的两种方式-拦截器和切面(具体代码)

springboot 日志记录接口的请求参数和响应结果的两种方式-拦截器和切面(具体代码)

前言:在生产中如果出现问题,我们想要查看日志,某个时间段用户调用接口的请求参数和响应的返回结果,通过日志来推测下用户当时做了什么操作。日志记录接口的请求参数和响应结果有利于我们排查生产的问题,但是也会给系统带来内存性能的问题。所以我们需要权衡其中的利弊来选择,下面就是记录日志两种方式的具体代码。

一、使用切面(推荐使用这种,简单)
@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];
                // 过滤不能转换成JSON的参数
                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("接口出入参日志打印切面处理请求参数异常", e);
        }
        return Arrays.toString(args);
    }

    /**
     * 返回结果简单处理
     * 1)把返回结果转成String,方便输出。
     * 2)返回结果太长则截取(最多3072个字符),方便展示。
     *
     * @param result 原方法调用的返回结果
     * @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);// 如果返回结果非String类型,转换成JSON格式的字符串
            }

            if (resultStr.length() > 3072) {
                resultStr = resultStr.substring(0, 3072);
            }
        } catch (Exception e) {
            resultStr = result.toString();
            log.error("接口出入参日志打印切面处理返回参数异常", e);
        }
        return resultStr;
    }
}
二、使用拦截器

本来以为这种方式只要自定义拦截器继承HandlerInterceptorAdapter类重写preHandle() 和 afterCompletion()就可以了。没想到还是遇到挺多坑。

请求参数:request.getParameterMap()可以获取到请求参数,但是如果接口是使用@RequestBody,就会发现得不到值。

响应结果:这是比较蛋疼一点,我几乎查了response的所有方法,都发现没法得到接口响应结果。

下面直接贴代码。

2.1 实现Filter类

流是一次性的,为了防止在日志中读取到流的请求参数,影响实际请求的接口

@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);
        }
        //获取请求中的流如何,将取出来的字符串,再次转换成流,然后把它放入到新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重新,让它能重复获取到body里的内容,这样才不会影响后续的流程
     * @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 实现ResponseBodyAdvice类

在实现ResponseBodyAdvice,就可以获取到接口响应的结果来打印日志,但是为了统一在自定义的拦截器处理日志,这里把响应的结果存在response的一个属性里。

@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拦截里取出并打印出返回结果
        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 实现WebMvcConfigurer
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Autowired
    private LogMonitorInterceptor logMonitorInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        // 接口操作日志拦截器
        registry.addInterceptor(logMonitorInterceptor);
    }
}

你可能感兴趣的:(java,springboot,spring,boot,java,后端)