如何优雅记录 http 请求/ 响应数据?

简介: 经常会遇到需要处理 http 请求以及响应 body 的场景。而这里比较大的一个问题是 servle t的 requestBody 或 responseBody 流一旦被读取了就无法二次读取了。

经常会遇到需要处理 http 请求以及响应 body 的场景。


而这里比较大的一个问题是 servle t的 requestBody 或 responseBody 流一旦被读取了就无法二次读取了。


针对这个问题,spring本身提供了解决方案,即:


ContentCachingRequestWrapper

ContentCachingResponseWrapper。

我们编写一个过滤器:

public abstract class HttpBodyRecorderFilter extends OncePerRequestFilter {
    private static final int DEFAULT_MAX_PAYLOAD_LENGTH = 1024 * 512;
    private int maxPayloadLength = DEFAULT_MAX_PAYLOAD_LENGTH;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        boolean isFirstRequest = !isAsyncDispatch(request);
        HttpServletRequest requestToUse = request;
        if (isFirstRequest && !(request instanceof ContentCachingRequestWrapper)
            && (request.getMethod().equals(HttpMethod.PUT.name())
                        || request.getMethod().equals(HttpMethod.POST.name()))) {
            requestToUse = new ContentCachingRequestWrapper(request);
        }
        HttpServletResponse responseToUse = response;
        if (!(response instanceof ContentCachingResponseWrapper) && (request.getMethod().equals(HttpMethod.PUT.name())
                        || request.getMethod().equals(HttpMethod.POST.name()))) {
            responseToUse = new ContentCachingResponseWrapper(response);
        }
        boolean hasException = false;
        try {
            filterChain.doFilter(requestToUse, responseToUse);
        } catch (final Exception e) {
            hasException = true;
            throw e;
        } finally {
            int code = hasException ? 500 : response.getStatus();
            if (!isAsyncStarted(requestToUse) && (this.codeMatched(code, AdvancedHunterConfigManager.recordCode()))) {
                recordBody(createRequest(requestToUse), createResponse(responseToUse));
            } else {
                writeResponseBack(responseToUse);
            }
        }
    }
    protected String createRequest(HttpServletRequest request) {
        String payload = "";
        ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
        if (wrapper != null) {
            byte[] buf = wrapper.getContentAsByteArray();
            payload = genPayload(payload, buf, wrapper.getCharacterEncoding());
        }
        return payload;
    }
    protected String createResponse(HttpServletResponse resp) {
        String response = "";
        ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(resp, ContentCachingResponseWrapper.class);
        if (wrapper != null) {
            byte[] buf = wrapper.getContentAsByteArray();
            try {
                wrapper.copyBodyToResponse();
            } catch (IOException e) {
                e.printStackTrace();
            }
            response = genPayload(response, buf, wrapper.getCharacterEncoding());
        }
        return response;
    }
    protected void writeResponseBack(HttpServletResponse resp) {
        ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(resp, ContentCachingResponseWrapper.class);
        if (wrapper != null) {
            try {
                wrapper.copyBodyToResponse();
            } catch (IOException e) {
                LOG.error("Fail to write response body back", e);
            }
        }
    }
    private String genPayload(String payload, byte[] buf, String characterEncoding) {
        if (buf.length > 0 && buf.length < getMaxPayloadLength()) {
            try {
                payload = new String(buf, 0, buf.length, characterEncoding);
            } catch (UnsupportedEncodingException ex) {
                payload = "[unknown]";
            }
        }
        return payload;
    }
    public int getMaxPayloadLength() {
        return maxPayloadLength;
    }
    private boolean codeMatched(int responseStatus, String statusCode) {
        if (statusCode.matches("^[0-9,]*$")) {
            String[] filteredCode = statusCode.split(",");
            return Stream.of(filteredCode).map(Integer::parseInt).collect(Collectors.toList()).contains(responseStatus);
        } else {
            return false;
        }
    }
    protected abstract void recordBody(String payload, String response);
    protected abstract String recordCode();
}

这样自定义一个filter继承HttpBodyRecorderFilter,重写recordBody方法就能自定义自己的处理逻辑了。


另外,recordCode方法可用于定义在请求响应码为多少的时候才会去记录body,例如可以定义为只有遇到400或500时才记录body,用于错误侦测。


过滤器的匹配规则比较简单,如果想要像springmvc那样进行匹配,我们可以使用:AntPathMatcher。

class PatternMappingFilterProxy implements Filter {
    private final Filter delegate;
    private final List<String> pathUrlPatterns = new ArrayList();
    private PathMatcher pathMatcher;
    public PatternMappingFilterProxy(Filter delegate, String... urlPatterns) {
        Assert.notNull(delegate, "A delegate Filter is required");
        this.delegate = delegate;
        int length = urlPatterns.length;
        pathMatcher = new AntPathMatcher();
        for (int index = 0; index < length; ++index) {
            String urlPattern = urlPatterns[index];
            this.pathUrlPatterns.add(urlPattern);
        }
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String path = httpRequest.getRequestURI();
        if (this.matches(path)) {
            this.delegate.doFilter(request, response, filterChain);
        } else {
            filterChain.doFilter(request, response);
        }
    }
    private boolean matches(String requestPath) {
        for (String pattern : pathUrlPatterns) {
            if (pathMatcher.match(pattern, requestPath)) {
                return true;
            }
        }
        return false;
    }
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        this.delegate.init(filterConfig);
    }
    @Override
    public void destroy() {
        this.delegate.destroy();
    }
    public List<String> getPathUrlPatterns() {
        return pathUrlPatterns;
    }
    public void setPathUrlPatterns(List<String> urlPatterns) {
        pathUrlPatterns.clear();
        pathUrlPatterns.addAll(urlPatterns);
    }
}

这样子,PatternMappingFilterProxy装饰了真正的HttpBodyRecorderFilter,支持传入urlPatterns,从而实现像springmvc那样的ant style的匹配。例如对于以下接口:

@PostMapping("/test/{id}")
public Object test(@PathVariable(value =  "id",required =  true)  final Integer index)  {
 //do something
}

可以设置urlPattern为/test/{id:[0-9]+}

相关文章
|
16天前
|
Java
java原生发送http请求
java原生发送http请求
|
24天前
|
网络协议 Linux iOS开发
推荐:实现RTSP/RTMP/HLS/HTTP协议的轻量级流媒体框架,支持大并发连接请求
推荐:实现RTSP/RTMP/HLS/HTTP协议的轻量级流媒体框架,支持大并发连接请求
52 1
|
6天前
|
安全 Java 网络安全
Servlet 教程 之 Servlet 客户端 HTTP 请求 2
Servlet教程介绍了如何在Servlet中处理HTTP请求,包括获取Cookie、头信息、参数、Session等。方法如:`getCookies()`、`getAttributeNames()`、`getHeaderNames()`、`getParameterNames()`等。还能获取身份验证类型、字符编码、MIME类型、请求方法、远程用户信息、URL路径、安全通道状态以及请求内容长度等。此外,可通过`getSession()`创建或获取Session,并以`Map`形式获取参数。
20 8
|
5天前
|
XML Java 数据格式
Servlet 教程 之 Servlet 服务器 HTTP 响应 3
`Servlet`教程示例展示了如何创建一个HTTP响应,使用`@WebServlet(&quot;/Refresh&quot;)`的`Refresh`类继承`HttpServlet`。在`doGet`方法中,设置了`Refresh`头以每5秒自动刷新,并用`setContentType(&quot;text/html;charset=UTF-8&quot;)`设定内容类型。还使用`Calendar`和`SimpleDateFormat`获取并格式化当前时间显示。相应的`web.xml`配置指定了Servlet路径。当访问此Servlet时,页面将每5秒更新一次显示的系统时间。
16 4
|
2天前
|
JSON 测试技术 API
Python的Api自动化测试使用HTTP客户端库发送请求
【4月更文挑战第18天】在Python中进行HTTP请求和API自动化测试有多个库可选:1) `requests`是最流行的选择,支持多种请求方法和内置JSON解析;2) `http.client`是标准库的一部分,适合需要低级别控制的用户;3) `urllib`提供URL操作,适用于复杂请求;4) `httpx`拥有类似`requests`的API,提供现代特性和异步支持。根据具体需求选择,如多数情况`requests`已足够。
8 3
|
9天前
|
安全 网络安全 开发工具
对象存储oss使用问题之flutter使用http库进行post请求文件上传返回400如何解决
《对象存储OSS操作报错合集》精选了用户在使用阿里云对象存储服务(OSS)过程中出现的各种常见及疑难报错情况,包括但不限于权限问题、上传下载异常、Bucket配置错误、网络连接问题、跨域资源共享(CORS)设定错误、数据一致性问题以及API调用失败等场景。为用户降低故障排查时间,确保OSS服务的稳定运行与高效利用。
32 1
|
10天前
|
网络协议 安全 API
Android网络和数据交互: 什么是HTTP和HTTPS?在Android中如何进行网络请求?
HTTP和HTTPS是网络数据传输协议,HTTP基于TCP/IP,简单快速,HTTPS则是加密的HTTP,确保数据安全。在Android中,过去常用HttpURLConnection和HttpClient,但HttpClient自Android 6.0起被移除。现在推荐使用支持TLS、流式上传下载、超时配置等特性的HttpsURLConnection进行网络请求。
10 0
|
1月前
|
JSON 前端开发 数据格式
糊涂工具类真是场景下请求http接口的案例
糊涂工具类真是场景下请求http接口的案例
21 0
|
1月前
|
数据采集 缓存 前端开发
http和https请求服务器的时候在请求头部分都带什么到服务器呢?
HTTP和HTTPS请求头基本结构相似,HTTPS多了一层SSL/TLS加密。常见请求头如Accept(指定内容类型)、Authorization(身份验证)、Cookie(会话跟踪)、User-Agent(标识用户代理)等。HTTPS特有的头包括Upgrade-Insecure-Requests(升级到HTTPS)、Strict-Transport-Security(强制使用HTTPS)、Sec-Fetch-*(安全策略)和X-Content-Type-Options、X-Frame-Options等(增强安全性)。实际应用中,请求头会根据需求和安全策略变化。
20 0
|
1月前
|
网络协议 网络安全 API
Qt 网络编程之美:探索 URL、HTTP、服务发现与请求响应
Qt 网络编程之美:探索 URL、HTTP、服务发现与请求响应
48 1