10.请求拦截和响应拦截

文章目录

前言

优秀的设计总是少不了丰富的扩展点, 比如spring可以自动装配, aop扩展, web模块也有拦截器, 甚至对servlet的过滤器都有封装; 再比如netty、doubbo等等都支持在数据流入流出都允许用户自定义扩展点实现定制化处理, 咱们的feign框架也同样如此, 在可以定制化组件的同时, 也允许我们对发起请求之前和接受请求之后根据扩展点实现个性化的处理。

前景回顾

  1. SynchronousMethodHandler#invoke方法中, 会先用参数填充模板得到有完整请求数据的载体RequestTemplate, 然后执行请求拦截器, 拦截器执行完成之后, 再讲目标请求地址设置给RequestTemplate, 最后构建客户端参数Request
  2. 在请求完成之后会在ResponseHandler#handleResponse方法中执行相应责任链, 该责任链的每一个节点在feign框架中都可以看作是一个响应拦截器

拦截器应用

请求拦截器

java 复制代码
public interface RequestInterceptor {

  /**
   * 拦截所有请求
   */
  void apply(RequestTemplate template);
}

我们可以在请求前给参数打印日志或者添加请求头啥的

下面定义两个请求拦截器RequestInterceptor

java 复制代码
/**
 * 请求头添加认证拦截器
 */
public class AuthHeardRequestInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        template.header("Authorization", "abc123");
    }
}

/**
 * 日志请求拦截器
 */
@Slf4j
public class LogRequestIntercept implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {

        Collection<String> requestVariables = template.getRequestVariables();
        log.info("请求行参数:{}", requestVariables);

        String s = new String(template.body(), StandardCharsets.UTF_8);
        log.info("请求体参数:{}", s);
    }
}

响应拦截器

打印返回时的数据

java 复制代码
public class LogResponseIntercept implements ResponseInterceptor {

    @Override
    public Object intercept(InvocationContext invocationContext, Chain chain) throws Exception {
        Response response = invocationContext.response();
        System.out.println("响应状态Status: " + response.status());
        System.out.println("响应头Headers: " + response.headers());

        // 流只能第一次
//        byte[] bodyData = Util.toByteArray(response.body().asInputStream());
//        String responseStr = StandardCharsets.UTF_8.newDecoder().decode(ByteBuffer.wrap(bodyData)).toString();
//        String s = new String(bodyData, StandardCharsets.UTF_8);
//        System.out.println("响应内容为: " + responseStr);
        Object result = chain.next(invocationContext);
//
        ObjectMapper objectMapper = new ObjectMapper();
        String jsonString = objectMapper.writeValueAsString(result);
        System.out.println("响应内容为: " + jsonString);

        return result;
    }
}

测试

写个测试controller

java 复制代码
@PostMapping("/feign/header")
public Person header(@RequestBody Person person, @RequestHeader Map<String, Object> header) {
    System.out.println("uncleqiao 收到body:" + person);
    System.out.println("uncleqiao 收到header:" + header);
    person.setName("小乔同学");
    person.setAge(20);
    return person;
}

feign接口

java 复制代码
public interface DemoClient4 {

    @RequestLine("POST /feign/header")
    @Headers("Content-Type: application/json")
    Person header(@Param String name, @Param Integer age);
}

测试类

java 复制代码
@Test
void interceptFunc() {
    JavaTimeModule javaTimeModule = new JavaTimeModule();
    DemoClient4 client = Feign.builder()
            .logLevel(feign.Logger.Level.FULL)
            .encoder(new JacksonEncoder(List.of(javaTimeModule)))
            .decoder(new JacksonDecoder(List.of(javaTimeModule)))
            .requestInterceptor(new LogRequestIntercept())
            .requestInterceptor(new AuthHeardRequestInterceptor())
            .responseInterceptor(new LogResponseIntercept())
            .logger(new Slf4jLogger())
            .target(DemoClient4.class, "http://localhost:8080");
    Person result = client.header("zs", 18);
    System.out.println("收到结果:" + result);
}

结果

java 复制代码
// controller打印
uncleqiao 收到body:Person(name=zs, age=18, gender=null, birthday=null)
uncleqiao 收到header:{authorization=abc123, content-type=application/json, accept=*/*, user-agent=Java/17.0.7, host=localhost:8080, connection=keep-alive, content-length=33}

// 请求拦截器打印
请求行参数:[]
请求体参数:{
  "name" : "zs",
  "age" : 18
}

// 响应拦截器打印
响应状态Status: 200
响应头Headers: {connection=[keep-alive], content-type=[application/json], date=[Sun, 01 Dec 2024 04:21:18 GMT], keep-alive=[timeout=60], transfer-encoding=[chunked]}

注意

这里定义的响应拦截器LogResponseIntercept中, 打印的返回结果是在责任链执行完的后面执行的, 也就是下面这段

java 复制代码
Object result = chain.next(invocationContext);
ObjectMapper objectMapper = new ObjectMapper();
String jsonString = objectMapper.writeValueAsString(result);
System.out.println("响应内容为: " + jsonString);

由于SocketInputStream io流只能读取一次, 如果我们在执行前使用如下方式读取了, 那么责任链后面的节点包括InvocationContext都无法再继续读取, 会抛出流已关闭异常

java 复制代码
        byte[] bodyData = Util.toByteArray(response.body().asInputStream());
        String responseStr = StandardCharsets.UTF_8.newDecoder().decode(ByteBuffer.wrap(bodyData)).toString();
//        String s = new String(bodyData, StandardCharsets.UTF_8);
        System.out.println("响应内容为: " + responseStr);

但是细心的同学可能发现当设置日志级别为logLevel(feign.Logger.Level.FULL)的时候, 也是可以在执行整个责任链之前读取流中的数据打印日志的

原因

ResponseHandler#handleResponse方法中, 在执行拦截链之前会有个logAndRebufferResponseIfNeeded方法来打印日志,

其中有段代码是这样的

java 复制代码
// 请求体内容 response.body():Response$InputStreamBody
byte[] bodyData = Util.toByteArray(response.body().asInputStream());
bodyLength = bodyData.length;
if (logLevel.ordinal() >= Level.FULL.ordinal() && bodyLength > 0) {
  // 打印响应内容
  log(configKey, "%s", decodeOrDefault(bodyData, UTF_8, "Binary data"));
}
log(configKey, "<--- END HTTP (%s-byte body)", bodyLength);
// 重建响应体, 因为SocketInputStream流只能读取一次, 所以必须重新设置
// 这里设置的是body是ByteArrayBody类型, 它可以重复读
return response.toBuilder().body(bodyData).build();
  1. response.body().asInputStream() 底层用的就是SocketInputStream , 所以默认只能读取一次

  2. Util.toByteArray方法将流中的字节都复制出来, 并关闭了原来的SocketInputStream

    java 复制代码
    public static byte[] toByteArray(InputStream in) throws IOException {
        checkNotNull(in, "in");
        try {
          ByteArrayOutputStream out = new ByteArrayOutputStream();
          copy(in, out);
          return out.toByteArray();
        } finally {
          ensureClosed(in);
        }
      }
  3. 将复制出来的字节重新设置到了body中, 这个body是ByteArrayBody对象, 里面的数据可以多次读取

    java 复制代码
    public Builder body(byte[] data) {
        this.body = ByteArrayBody.orNull(data);
        return this;
    }
    private static Body orNull(byte[] data) {
        if (data == null) {
          return null;
        }
        return new ByteArrayBody(data);
    }

    这就是为什么当设置feign的日志级别是feign.Logger.Level.FULL的时候, 我们可以在拦截器中先读取数据, 再执行拦截器的原理。不过有打印返回数据的需求, 还是建议在执行响应拦截器之后再打印, 因为日志级别一般也不是开到FULL这么高。

响应拦截器原理

关于响应拦截器的责任链, 在第6篇中有详细介绍过, 这里有必要再拿出来说说

java 复制代码
protected ResponseInterceptor.Chain responseInterceptorChain() {
    ResponseInterceptor.Chain endOfChain =
        ResponseInterceptor.Chain.DEFAULT;
    ResponseInterceptor.Chain executionChain = this.responseInterceptors.stream()
        .reduce(ResponseInterceptor::andThen)
        .map(interceptor -> interceptor.apply(endOfChain))
        .orElse(endOfChain);

    return (ResponseInterceptor.Chain) Capability.enrich(executionChain,
        ResponseInterceptor.Chain.class, capabilities);
  }

1.reduce

java 复制代码
Optional<T> reduce(BinaryOperator<T> accumulator)
    
 public interface BinaryOperator<T> extends BiFunction<T,T,T>{...}

public interface BiFunction<T, U, R> {
    R apply(T t, U u);
}
  1. reduce方法接收一个BinaryOperator参数, 函数式接口的方法签名为R apply(T t, U u);, 由于BinaryOperator定义了父类的泛型是<T,T,T>; 所以方法签名可以看作是T apply(T t, T u), 也就是接受两个相同类型的变量, 然后返回的也是同一个类型。

  2. reduce会遍历每一个集合对象, 属于累加型, 类似于 a = 0; for (i = 0; i++; i<list.size) a = sum(a,i); 这种, 也就是先定义一个累加值, 然后累加的值会和每一个集合元素用BinaryOperator 的apply方法处理

  3. ResponseInterceptor::andThen是一个类的非静态方法引用式lambda表达式, 它会默认传入一个当前实例作为调用对象, 也就是responseInterceptor.andThen(..), 聚合的拦截器就会和每一个拦截器用andThen方法进行处理

  4. andThan方法如下, 它会用匿名lambda的形式创建一个ResponseInterceptor, 然后拦截器内部nextContext -> nextInterceptor.intercept又是用一个匿名lambda形式创建了一个Chain, 这个Chain中包含了当前拦截器的调用nextInterceptor.intercept, 同时它使用的第二个参数是上一个拦截器传过来的chain, 而不是当前创建的nextContext

java 复制代码
default ResponseInterceptor andThen(ResponseInterceptor nextInterceptor) {
    return (ic, chain) -> intercept(ic,
        nextContext -> nextInterceptor.intercept(nextContext, chain));
  }
  1. 也就是说andThen方法创建了一个拦截器, 同时也创建了一个Chain对象作为该拦截器的第二个参数, 然后当前拦截器nextInterceptor.intercept放在这个Chain具体实现对象的调用方法里, 也就是next; 换句话说调用Chain.next 就是调用nextInterceptor#intercept

  2. 所以reduce每一次遍历都会创建一个新的拦截器, 并且创建一个Chain在拦截器内部调用给它的调用函数intercept, 同时调用函数中执行了当前拦截器, 所以就形成了一个调用链, 前面一个拦截器的intercept中调用了当前拦截器nextInterceptor, 只不过每次调用的Chain参数都是新创建的

  3. map方法就比较简单了, 它调用聚合之后的拦截器的apply方法, 该方法创建了一个Chain, 实际调用的时候参数传入的是InvocationContext对象, 在内部调用当前聚合拦截器的intercept方法, 第一个参数是InvocationContext对象, 第二个就是ResponseInterceptor.Chain.DEFAULT

    java 复制代码
    default Chain apply(Chain chain) {
    	// 调用当前拦截器的intercept方法; 这里就是聚合的拦截器
    	return request -> this.intercept(request, chain);
    }

它平铺之后如下

java 复制代码
public ResponseInterceptor.Chain buildRespChain() {
        ResponseInterceptor.Chain endOfChain = ResponseInterceptor.Chain.DEFAULT;

        // 合并所有拦截器成一个 ResponseInterceptor; 这个定义和循环的动作对应reduce
        ResponseInterceptor combinedInterceptor = null;
        for (ResponseInterceptor interceptor : this.responseInterceptors) {
            if (combinedInterceptor == null) {
                combinedInterceptor = interceptor;
            } else {
                ResponseInterceptor previousCombinedInterceptor = combinedInterceptor;
                // 这个聚合赋值的动作也对应reduce
                // 这个创建拦截器的动作对应andThen的(ic, chain) -> intercept(...)动作
                combinedInterceptor = new ResponseInterceptor() {
                    @Override
                    public Object intercept(InvocationContext ic, Chain chain) throws Exception {
                        // 这个new Chain对应andThen的nextContext -> nextInterceptor.intercept(nextContext, chain)动作
                        return previousCombinedInterceptor.intercept(ic, new Chain() {
                            @Override
                            public Object next(InvocationContext context) throws Exception {
                                return interceptor.intercept(context, chain);
                            }
                        });
                    }
                };
            }
        }
        // 如果没有拦截器,直接返回 endOfChain
        if (combinedInterceptor == null) {
            return endOfChain;
        }
        ResponseInterceptor temp = combinedInterceptor;
        // 使用 apply 构造最终责任链
        return new ResponseInterceptor.Chain() {
            @Override
            public Object next(InvocationContext request) throws Exception {
                return temp.intercept(request, endOfChain);
            }
        };
    }

总结

  1. 请求拦截器需要实现RequestInterceptor接口, 它在真正使用客户端执行调用前执行, 可以用它来处理请求头, 打印日志啥的
  2. 响应拦截器在客户端请求成功后处理, 默认不支持在拦截链之前完之前打印响应对象, 因为SocketInputStream只能读取一次, 但是开启feign的日志级别为HEADERSFULL可以打破这个限制
  3. 响应拦截器使用stream流组装, 显得晦涩难懂, 需要大家多揣摩
相关推荐
优雅的落幕7 分钟前
多线程---线程安全(synchronized)
java·开发语言·jvm
Charlie__ZS8 分钟前
帝可得-设备管理
java·maven·intellij-idea·idea
爱上语文14 分钟前
请求响应:常见参数接收及封装(数组集合参数及日期参数)
java·开发语言·spring boot·后端
孙同学_17 分钟前
【Linux篇】权限管理 - 用户与组权限详解
java·linux·服务器
CQU_JIAKE26 分钟前
926[study]Docker,DHCP
java·开发语言
浪 子29 分钟前
SpringBoot mq快速上手
java·spring boot·spring
程序猿进阶30 分钟前
Tomcat 都有哪些核心组件
java·开发语言·后端·面试·性能优化·tomcat·firefox
码农阿豪1 小时前
极兔速递开放平台快递物流查询API对接流程
java·极兔开放平台·物流查询api
小金的学习笔记1 小时前
Java开发利器:IDEA的安装与使用(下)
java·开发语言·intellij-idea
一只小灿灿1 小时前
Java 网络编程从入门到精通
java·开发语言·网络