记录RestTemplate日志,javassist重写class,使响应流可重读

记录RestTemplate日志

前言

最近在做的项目经常需要与微信服务器交互,选用了restTemplate作为请求工具。但由于对微信API的不熟悉,导致请求时经常会出错,由此产生了记录restTemplate请求与响应日志的想法。

过程

接口实现

实际上RestTemplate已经为我们提供了拦截器的接口,我们只需要实现该接口,并实现其中的指定方法,就可以获取到RestTemplate的每一次请求。

@FunctionalInterface
public interface ClientHttpRequestInterceptor {
    ClientHttpResponse intercept(HttpRequest var1, byte[] var2, ClientHttpRequestExecution var3) throws IOException;
}

日志记录

拦截器记录日志具体代码
当发起请求时,RestTemplate会将本次请求的请求对象、请求参数及请求执行者,传递给拦截器。
我们只需在请求调用执行前,将请求对象,及请求参数进行记录,然后在使用执行者发送请求,再将本次请求返回的响应进行记录即可。


import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import sun.net.www.protocol.http.HttpURLConnection;

import java.io.*;

@Slf4j
public class LoggingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        traceRequest(request, body);
        ClientHttpResponse response = execution.execute(request, body);
        traceResponse(response);
        return response;
    }
//记录请求体及请求参数
    private void traceRequest(HttpRequest request, byte[] body) throws IOException {
        log.info("===========================request begin================================================");
        log.info("URI         : {}", request.getURI());
        log.info("Method      : {}", request.getMethod());
        log.info("Headers     : {}", request.getHeaders());
        log.info("Request body: {}", new String(body, "UTF-8"));
        log.info("==========================request end================================================");
    }
//记录响应信息及响应数据
    private void traceResponse(ClientHttpResponse response) throws IOException {
        StringBuilder inputStringBuilder = new StringBuilder();
        // 获取响应流
        InputStream body = response.getBody();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(body));
        String line = bufferedReader.readLine();
        while (line != null) {
            inputStringBuilder.append(line);
            inputStringBuilder.append('\n');
            line = bufferedReader.readLine();
        }
        log.info("============================response begin==========================================");
        log.info("Status code  : {}", response.getStatusCode());
        log.info("Status text  : {}", response.getStatusText());
        log.info("Headers      : {}", response.getHeaders());
        log.info("Response body: {}", inputStringBuilder.toString());//WARNING: comment out in production to improve performance
        log.info("=======================response end=================================================");
    }

}

将拦截器添加进RestTemplate中

@Configuration
public class OtherConfig {
    @Bean
    public RestTemplate restTemplate(ClientHttpRequestFactory factory, @Value("${spring.http.encoding.charset}")String charset){

        RestTemplate restTemplate = new RestTemplate(factory);
//        获取转换器集合
        List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters();
        if (messageConverters==null){
            messageConverters=new ArrayList<>();
        }
        //新建一个json转换器,用来处理请求与响应的json数据
        GsonHttpMessageConverter converter = new GsonHttpMessageConverter();
        // 设置该转换器支持类型为所有
        converter.setSupportedMediaTypes(Collections.singletonList(MediaType.ALL));
        // 设置该转换器字符集
        converter.setDefaultCharset(Charset.forName(charset));
        messageConverters.add(converter);
        restTemplate.setMessageConverters(messageConverters);
        // 新建拦截器,并添加进restTemplate实例中
        restTemplate.getInterceptors().add(new LoggingClientHttpRequestInterceptor());
        return restTemplate;
    }

    @Bean
    public ClientHttpRequestFactory simpleClientHttpRequestFactory(){
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setConnectTimeout(15000);
        factory.setReadTimeout(5000);
        return factory;
    }
}

至此,记录RestTemplate请求日志的需求就已完成

重读响应流

流的重读

java中的部分流是可以进行重读的,只需调用该流实例的mark()方法,传入int:readlimit值,即可在流此时的位置添加书签。
mark()中的readlimit,表示在mark()之后,只要该流被继续读取的长度在此int值范围内,此mark就有效,超出则失效。
标记书签后,只需调用该流的reset()方法,就可以将流重回最后一次mark的位置上,实现流的重复使用。
我们可以调用流的markSupported(),查看此流实例是否支持mark()重读。
RestTemplate所提供的InputStream实现类:HttpURLConnection$HttpInputStream直接继承于FilterInputStream,FilterInputStream又继承于InputStream,该类的markSupported()返回false,表不支持,所以无法对响应流进行重读。
但我们在RestTemplate的日志记录时,已经将该流读取,之后业务再用到响应流数据时,再读取流时会报空指针异常,所以此时只能考虑放弃记录响应数据,或是重写RestTemplate底层,将RestTemplate的响应流替换为可重读的InputStream子类。

底层替换

底层的实现的替换,可以使用java的反射机制,强制将可重读流写入RestTemplate响应流的位置上即可。
由于最近有在学习javassist,对在运行中class字节码的重写,所以我在这里用了重构字节码对象的方式,进行流的替换。

实现

依赖导入

<dependency>
	<groupId>org.javassist</groupId>
	<artifactId>javassist</artifactId>
	<version>3.25.0-GA</version>
</dependency>

然后只需要在拦截器的构造器中,对指定class的方法进行重写即可

public LoggingClientHttpRequestInterceptor() {
        try {
        // 获取class字节池
            final ClassPool classPool = ClassPool.getDefault();
            //获取需要修改的class对象,RestTemplate底层使用的是SimpleClientHttpResponse
            CtClass ctClass = classPool.get("org.springframework.http.client.SimpleClientHttpResponse");
            // 获取 获取流的方法
            CtMethod getBody = ctClass.getDeclaredMethod("getBody");
            // 重写该方法的具体实现
            String methodBody ="{if(responseStream==null){java.io.InputStream errorStream = this.connection.getErrorStream();\n" +
                    "this.responseStream = new java.io.BufferedInputStream(errorStream != null ? errorStream : this.connection.getInputStream());" +
                    "}return this.responseStream;}";
//                覆盖原方法方法
            getBody.setBody(methodBody);
            //将编辑后的对象转换成真正的class对象
            ctClass.toClass();
        } catch (NotFoundException e) {
            e.printStackTrace();
        } catch (CannotCompileException e) {
            e.printStackTrace();
        }
    }

替换流后,我们还需修改拦截器中,记录响应数据的方法。
实际上只需要在获取流后,进行mark即可

        StringBuilder inputStringBuilder = new StringBuilder();
        BufferedInputStream body = (BufferedInputStream) response.getBody();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(body));
        body.mark(1024);
        String line = bufferedReader.readLine();
        while (line != null) {
            inputStringBuilder.append(line);
            inputStringBuilder.append('\n');
            line = bufferedReader.readLine();
        }
        body.reset();

到这里,就完成了对RestTemplate的日志记录。

借鉴资料

借鉴:RestTemplate日志记录
借鉴:Java中,流的重读原理
借鉴:字节码增强技术-Javassist

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值