优雅地记录http请求和响应的数据

1.场景

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

2.解决方案

spring 提供了两个类,如下:

  • ContentCachingRequestWrapper
  • ContentCachingResponseWrapper

代码如下:

package com.sx.system.config;

import lombok.extern.java.Log;
import org.springframework.http.HttpMethod;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@WebFilter(filterName = "httpbodyfileter")
@Log
public  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, "200"))) {
                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.info("Cannot copy Fail to write response body back");
            }
        }

    }

    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  void recordBody(String payload, String response){
        log.info("payload:"+payload);
        log.info("response:"+response);
    };



}

3.原理

通过ContentCachingResponseWrapper缓存类来作中转,这边用了装饰模式,把对外输出的内容先缓存在ContentCachingResponseWrapper对象的FastByteArrayOutputStream content对象中,输出日志的时候先从content对象中拿,输出完再真正的写入到输出对象中。👌,下面我们来看代码。

3.1 初始化

初始化的入口
在这里插入图片描述
原来的response 会被保存到这边
在这里插入图片描述

3.2 缓存处理

Controller处理完业务,返回时先缓存在Response(ContentCachingResponseWrapper)对象的FastByteArrayOutputStream content对象中。
以下是Controller的代码
在这里插入图片描述
下面按三个步聚来分析
在这里插入图片描述

A 是SpringMVC 处理中心 DispatcherServlet 处理Controller的入口
在这里插入图片描述
注意,这边传入的正是我们在记录日志过滤中输入的缓存包装类

B Spring 的返回处理中心把字符串(hellow world )写到out(ContentCachingResponseWrapper#ResponseServletOutputStream)中
在这里插入图片描述
C 通过ContentCachingResponseWrapper#ResponseServletOutputStream写入到
private final FastByteArrayOutputStream content 对对象中。代码如下
在这里插入图片描述

3.3 输出日志并且回写代理的Response中

在这里插入图片描述
在这里插入图片描述
接下来我们来看下 byte[] buf = wrapper.getContentAsByteArray()方法
#ContentCachingResponseWrapper

	public byte[] getContentAsByteArray() {
		return this.content.toByteArray();
	}

再来看下以下方法
#ContentCachingResponseWrapper.copyBodyToResponse();

public void copyBodyToResponse() throws IOException {
		copyBodyToResponse(true);
	}

	/**
	 * Copy the cached body content to the response.
	 * @param complete whether to set a corresponding content length
	 * for the complete cached body content
	 * @since 4.2
	 */
	protected void copyBodyToResponse(boolean complete) throws IOException {
		if (this.content.size() > 0) {
			HttpServletResponse rawResponse = (HttpServletResponse) getResponse();
			if ((complete || this.contentLength != null) && !rawResponse.isCommitted()) {
				if (rawResponse.getHeader(HttpHeaders.TRANSFER_ENCODING) == null) {
					rawResponse.setContentLength(complete ? this.content.size() : this.contentLength);
				}
				this.contentLength = null;
			}
			this.content.writeTo(rawResponse.getOutputStream());
			this.content.reset();
			if (complete) {
				super.flushBuffer();
			}
		}
	}

主要是把缓存中的内容放到所代理的Response中。👌,这样就解决了输入、输出流只能一次读写的问题。

4.参考

1.RestTemplate打印响应结果
https://blog.csdn.net/zhang_Red/article/details/91960182
2.RestTemplate相关组件:ClientHttpRequestInterceptor【享学Spring MVC】
https://www.cnblogs.com/yourbatman/p/11532777.html

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

山巅

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值