Bootstrap

记录请求过程,包括请求体参数、响应结果(基于Servlet Filter实现)

一、需求

如题。

二、思路与分析

  1. 拦截器无法实现如题的功能,因为请求体数据和响应数据获取一次后就清空了,若多次获取会报异常,且无法对请求进行包装。
  2. 过滤器可以在doFilter中放入请求和响应,所以可以在filter对请求进行包装,以便进行二次获取。但是无法直接注入,可以利用ApplicationContextAware解决或直接使用OncePerRequestFilter。
  3. 切面也可实现功能。

本文不采用切面,而是通过过滤器实现

三、具体代码

请求包装类:

package com.lihenggen.demo.test.filter;

import cn.hutool.core.io.IoUtil;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

/**
 * @author lihg
 */
public class ContentCachingRequestWrapper extends HttpServletRequestWrapper{

    private byte[] body;

    private BufferedReader reader;

    private ServletInputStream inputStream;

    public ContentCachingRequestWrapper(HttpServletRequest request) throws IOException{
        super(request);
        loadBody(request);
    }

    private void loadBody(HttpServletRequest request) throws IOException{
        body = IoUtil.readBytes(request.getInputStream());
        inputStream = new RequestCachingInputStream(body);
    }

    public byte[] getBody() {
        return body;
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        if (inputStream != null) {
            return inputStream;
        }
        return super.getInputStream();
    }

    @Override
    public BufferedReader getReader() throws IOException {
        if (reader == null) {
            reader = new BufferedReader(new InputStreamReader(inputStream, getCharacterEncoding()));
        }
        return reader;
    }

    private static class RequestCachingInputStream extends ServletInputStream {

        private final ByteArrayInputStream inputStream;

        public RequestCachingInputStream(byte[] bytes) {
            inputStream = new ByteArrayInputStream(bytes);
        }

        @Override
        public int read() {
            return inputStream.read();
        }

        @Override
        public boolean isFinished() {
            return inputStream.available() == 0;
        }

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void setReadListener(ReadListener readlistener) {
        }

    }

}

过滤器代码:

package com.lihenggen.demo.test.filter;

import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONException;
import cn.hutool.json.JSONUtil;
import com.lihenggen.demo.test.util.IpUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.support.ResourcePatternUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.util.PatternMatchUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingResponseWrapper;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;


/**
 * @author lihg
 */
@Component
@Slf4j
//@Order(0)
//@WebFilter(filterName = "thirdPartRecordFilter", urlPatterns = "/restful/*")
public class ThirdPartRecordFilter extends OncePerRequestFilter {

    private static final String THIRD_PART_PATTERN = "/restful/**";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {

        // 只对第三方接口行拦截
        if (!new AntPathMatcher().match(THIRD_PART_PATTERN, request.getServletPath())) {
            chain.doFilter(request, response);
            return;
        }

        Long startTime = System.currentTimeMillis();
        // 转换成包装类,避免请求体和响应数据读取后数据为空的问题
        ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest) request);
        ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response);

        System.out.println("======================================================================================================");
        log.info("请求ip:{}", IpUtil.getIpAddr((HttpServletRequest) request));
        log.info("认证信息:{}", requestWrapper.getHeader("Authorization"));
        log.info("请求路径:{}", requestWrapper.getRequestURI());
        Map<String, String[]> parameterMap = requestWrapper.getParameterMap();
        parameterMap.keySet().stream().forEach(key -> {
            log.info("请求参数:{}" + key + " : " + ArrayUtil.join(parameterMap.get(key), ",", "[", "]"));
        });
        log.info(new String(requestWrapper.getBody(), request.getCharacterEncoding()));

        try {
            chain.doFilter(requestWrapper, responseWrapper);

            // httpStatus正常
            log.info("httpStatus:{}", responseWrapper.getStatus());
            byte[] content = responseWrapper.getContentAsByteArray();
            Long costTime = System.currentTimeMillis() - startTime;
            log.info("请求耗时:{}", costTime);
            // 记录响应具体内容
            if (ArrayUtil.isNotEmpty(content)) {
                String resultData = new String(content, responseWrapper.getCharacterEncoding());
                if (JSONUtil.isJson(resultData)) {
                    log.info("状态码:{}", StrUtil.toString(JSONUtil.getByPath(JSONUtil.parseObj(resultData), "code")));
                }
                log.info("请求结果: {}", resultData);
                // todo 持久化
                ServletOutputStream out = response.getOutputStream();
                out.write(content);
                out.flush();
            }
        } catch (Exception e) {
            // httpStatus异常或代码异常,注意,经过测试这里哪怕代码异常,前端接收的状态码为500,但responseWrapper.getStatus()仍为200
            log.info("httpStatus:{}", responseWrapper.getStatus());
            log.info("异常堆栈:【{}】", ExceptionUtil.stacktraceToString(e));
            // todo 持久化
            throw e;
        }

    }

}


获取请求IP地址工具类:

import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;

public class IpUtil {

    /**
     * 获取当前网络ip
     *
     * @param request
     * @return
     */
    public static String getIpAddr(HttpServletRequest request) {
        String 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 (ipAddress.equals("127.0.0.1") || ipAddress.equals("0:0:0:0:0:0:0:1")) {
                //根据网卡取本机配置的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) { //"***.***.***.***".length() = 15
            if (ipAddress.indexOf(",") > 0) {
                ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
            }
        }
        return ipAddress;
    }
}

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;