当前位置: 首页 > article >正文

10.请求拦截和响应拦截

文章目录

  • 前言
  • 前景回顾
  • 拦截器应用
    • 请求拦截器
    • 响应拦截器
    • 测试
    • 响应拦截器原理
  • 总结

前言

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

前景回顾

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

拦截器应用

请求拦截器

public interface RequestInterceptor {

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

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

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

/**
 * 请求头添加认证拦截器
 */
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);
    }
}

响应拦截器

打印返回时的数据

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

@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接口

public interface DemoClient4 {

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

测试类

@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);
}

结果

// 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中, 打印的返回结果是在责任链执行完的后面执行的, 也就是下面这段

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

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

        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方法来打印日志,

其中有段代码是这样的

// 请求体内容 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

    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对象, 里面的数据可以多次读取

    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篇中有详细介绍过, 这里有必要再拿出来说说

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

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

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

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

它平铺之后如下

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流组装, 显得晦涩难懂, 需要大家多揣摩

http://www.kler.cn/a/419100.html

相关文章:

  • unity实现计数器
  • LeetCode-315. Count of Smaller Numbers After Self
  • 力扣难题解析
  • CCF 第一届算法竞赛 CACC 考题回忆
  • 【热门主题】000077 物联网智能项目:开启智能未来的钥匙
  • MVC core 传值session
  • Rust代写 OCaml代做 Go R语言 SML Haskell Prolog DrRacket Lisp
  • Jackson库--ObjecMapper
  • vue3 与 spring-boot 完成跨域访问
  • Maven java 项目,想执行verify阶段指令,通常需要配置哪些插件呢?
  • YOLOv8-ultralytics-8.2.103部分代码阅读笔记-ops.py
  • Java知识及热点面试题总结(二)
  • 远程桌面协助控制软件 RustDesk v1.3.3 多语言中文版
  • 精准用户获取与私域流量运营:多商户链动 2+1 模式商城小程序的赋能策略
  • Linux内核编译流程(Ubuntu24.04+Linux Kernel 6.8.12)
  • spring boot 调用C#封装的DLL文件中的函数
  • 力扣3372.连接两棵树后最大目标节点数目I
  • 内网使用docker搭建librespeed测速网站
  • 挑战用React封装100个组件【004】
  • UaGateway:实现OPC DA和OPC UA的高效转换
  • FFmpeg一些常用的命令
  • ElasticSearch的学习
  • JAVA中HashMap、TreeMap、LinkedHashMap 的用法与注意事项
  • 简单搭建qiankun的主应用和子应用并且用Docker进行服务器部署
  • AI高中数学教学视频生成技术:利用通义千问、MathGPT、视频多模态大模型,语音大模型,将4个模型融合 ,生成高中数学教学视频,并给出实施方案。
  • MySQL索引与分区:性能优化的关键