IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 开发测试 -> Feign 源码随便看看 -> 正文阅读

[开发测试]Feign 源码随便看看

Feign github 地址

Feign 能做什么 ?

? ? ? ? 借用 README.md 开头的第一句话?Feign makes writing java http clients easier。直接说明了 Feign 的作用是编写 java 的?http clients。

从一个简单的单元测试开始

? ? ? ? 添加?io.github.openfeign:feign-core:10.12??com.squareup.okhttp3:mockwebserver:3.14.9?坐标依赖。

import feign.Feign;
import feign.Param;
import feign.RequestLine;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class FeignTest {

    public static String baseUrl;

    private static final String DEFAULT_BODY = "hello, world!";

    @BeforeAll
    public static void init() {
        // 初始化 mock web server,用于 mock http请求
        MockWebServer server = new MockWebServer();
        MockResponse mockResponse = new MockResponse().setBody(DEFAULT_BODY);
        server.enqueue(mockResponse);
        baseUrl = server.url("/default").toString();
    }

    // 测试客户端的请求模板
    interface TestClient {

        @RequestLine("GET /test/{owner}")
        String test(@Param("owner") String owner);
    }

    @Test
    void testSimpleFeign() {
        // 使用 Feign 创建出 TestClient 的实例
        TestClient testClient = Feign.builder().target(TestClient.class, baseUrl);
        // 发起 http 调用
        String result = testClient.test("o");
        Assertions.assertEquals(DEFAULT_BODY, result);
    }
}

? ? ? ? 通过这个小程序可以看到 Feign 真的可以这个样子发起 http 请求调用,下面看看 Feign 是如何工作的。

? ? ? ? 初步猜测下要想让?TestClient#test() 方法工作起来大概需要以下几步:

  1. 有?TestClient 接口的实例。
  2. 解析调用方法中的注解获取参数去构建 http 协议。
  3. 真正的发起 http 调用。? ?

????????Feign.builder() 会得到 feign.Feign.Builder 这个类的实例。采用建造者模式构建 Feign 需要的相关配置。

public static class Builder {

    // 请求拦截器
    private final List<RequestInterceptor> requestInterceptors =
        new ArrayList<RequestInterceptor>();
    // 日志级别
    private Logger.Level logLevel = Logger.Level.NONE;
    // 解析接口方法注解
    private Contract contract = new Contract.Default();
    // http请求客户端
    private Client client = new Client.Default(null, null);
    // 重试策略
    private Retryer retryer = new Retryer.Default();
    private Logger logger = new NoOpLogger();
    private Encoder encoder = new Encoder.Default();
    private Decoder decoder = new Decoder.Default();
    private QueryMapEncoder queryMapEncoder = new FieldQueryMapEncoder();
    private ErrorDecoder errorDecoder = new ErrorDecoder.Default();
    // client的请求配置
    private Options options = new Options();
    // 接口方法的调用控制
    private InvocationHandlerFactory invocationHandlerFactory =
        new InvocationHandlerFactory.Default();
    private boolean decode404;
    private boolean closeAfterDecode = true;
    private ExceptionPropagationPolicy propagationPolicy = NONE;
    private boolean forceDecoding = false;
    private List<Capability> capabilities = new ArrayList<>();
    
    ......
}

? ? ? ? 特别关注下?Contract ( 解析方法中的相关注解),Client (真正发起 http 调用的客户端),InvocationHandlerFactory (接口代理方法的分发调用)。

/**
 * Controls reflective method dispatch.
 */
public interface InvocationHandlerFactory {

  InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch);

  /**
   * Like {@link InvocationHandler#invoke(Object, java.lang.reflect.Method, Object[])}, except for a
   * single method.
   */
  interface MethodHandler {

    Object invoke(Object[] argv) throws Throwable;
  }

  static final class Default implements InvocationHandlerFactory {

    @Override
    public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) {
      return new ReflectiveFeign.FeignInvocationHandler(target, dispatch);
    }
  }
}

? ? ? ? 在?InvocationHandlerFactory 源码中有个?MethodHandler 接口,上面的注释说明了类似 jdk 动态代理的?java.lang.reflect.InvocationHandler#invoke() 功能,真正执行方法的时候是委托给了?MethodHandler#invoke()。

????????这里 Feign 的大体样子就能想的出来了,那么这些东西是如何串联在一起的?TestClient 实例对象是什么时候创建的?

Feign 是如何生成接口的实现类??

????????Feign.builder().target() 方法返回了?TestClient 实例对象,所以进去看看做了什么。

public static class Builder {

    ......

    public <T> T target(Class<T> apiType, String url) {
      return target(new HardCodedTarget<T>(apiType, url));
    }

    public <T> T target(Target<T> target) {
      return build()
              // 此处为 ReflectiveFeign实现的newInstance
              .newInstance(target);
    }

    public Feign build() {
      // 构建 Feign 的相关组建
      Client client = Capability.enrich(this.client, capabilities);
      Retryer retryer = Capability.enrich(this.retryer, capabilities);
      List<RequestInterceptor> requestInterceptors = this.requestInterceptors.stream()
          .map(ri -> Capability.enrich(ri, capabilities))
          .collect(Collectors.toList());
      Logger logger = Capability.enrich(this.logger, capabilities);
      Contract contract = Capability.enrich(this.contract, capabilities);
      Options options = Capability.enrich(this.options, capabilities);
      Encoder encoder = Capability.enrich(this.encoder, capabilities);
      Decoder decoder = Capability.enrich(this.decoder, capabilities);
      InvocationHandlerFactory invocationHandlerFactory =
          Capability.enrich(this.invocationHandlerFactory, capabilities);
      QueryMapEncoder queryMapEncoder = Capability.enrich(this.queryMapEncoder, capabilities);

      // 初始化 SynchronousMethodHandler.Factory
      // SynchronousMethodHandler 是 MethodHandler 的实现之一
      // 执行具体的方法调用
      SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
          new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
              logLevel, decode404, closeAfterDecode, propagationPolicy, forceDecoding);

      // 通过 ParseHandlersByName 得到 
      // key 为 类名#方法名(参数类型),value 为 MethodHandler 的 map
      // 这里 contract 作为参数传到了 ParseHandlersByName 的构造函数
      ParseHandlersByName handlersByName =
          new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,
              errorDecoder, synchronousMethodHandlerFactory);
      // 最终得到 Feign 的实现类之一,将会调用重写的 newInstance 方法
      return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
    }
  }

  ......
}

? ? ? ? 到这里可以看出?TestClient 实例对象应该会被?ReflectiveFeign#newInstance() 创建出来。

public class ReflectiveFeign extends Feign {

    ......

  /**
   * creates an api binding to the {@code target}. As this invokes reflection, care should be taken
   * to cache the result.
   */
  @SuppressWarnings("unchecked")
  @Override
  public <T> T newInstance(Target<T> target) {
    // 通过ParseHandlersByName#apply 
    // 获取格式为 类名#方法名(参数类型) 的方法签名对应的 MethodHandler
    Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
    List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();

    // 将 nameToHandler 转换为 methodToHandler
    for (Method method : target.type().getMethods()) {
      if (method.getDeclaringClass() == Object.class) {
        continue;
      } else if (Util.isDefault(method)) {
        DefaultMethodHandler handler = new DefaultMethodHandler(method);
        defaultMethodHandlers.add(handler);
        methodToHandler.put(method, handler);
      } else {
        methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
      }
    }
    
    // jdk 的动态代理创建接口的实例对象
    InvocationHandler handler = factory.create(target, methodToHandler);
    T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
        new Class<?>[] {target.type()}, handler);

    for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
      defaultMethodHandler.bindTo(proxy);
    }
    return proxy;
  }

    ......
}

? ? ? ? 到这里可以看到是通过 jdk 的动态代理获取了?TestClient 实例对象。

Feign 是如何解析接口方法上的注解 ?

????????ParseHandlersByName#apply 方法进去看看。

  static final class ParseHandlersByName {

   ......

    public Map<String, MethodHandler> apply(Target target) {
      // 通过 contract 获取 MethodMetadata 列表
      List<MethodMetadata> metadata = contract.parseAndValidateMetadata(target.type());

   ......
    }

  ......
}

? ? ? ? 这个?MethodMetadata 就是封装好的注解解析的参数。那么?@RequestLine 这些注解在那里定义的?在使用 spring cloud openfeign 使用的是 spring mvc 的注解,那他是怎么自定义的?继续看Contract#parseAndValidateMetadata 方法。

abstract class BaseContract implements Contract {

    /**
     * @param targetType {@link feign.Target#type() type} of the Feign interface.
     * @see #parseAndValidateMetadata(Class)
     */
    @Override
    public List<MethodMetadata> parseAndValidateMetadata(Class<?> targetType) {
      // 各种检查
      checkState(targetType.getTypeParameters().length == 0, "Parameterized types unsupported: %s",
          targetType.getSimpleName());
      checkState(targetType.getInterfaces().length <= 1, "Only single inheritance supported: %s",
          targetType.getSimpleName());

      final Map<String, MethodMetadata> result = new LinkedHashMap<String, MethodMetadata>();
      for (final Method method : targetType.getMethods()) {
        if (method.getDeclaringClass() == Object.class ||
            (method.getModifiers() & Modifier.STATIC) != 0 ||
            Util.isDefault(method)) {
          continue;
        }

        // 创建 MethodMetadata targetType 为当前例子中的 TestClient
        final MethodMetadata metadata = parseAndValidateMetadata(targetType, method);
        if (result.containsKey(metadata.configKey())) {
          MethodMetadata existingMetadata = result.get(metadata.configKey());
          Type existingReturnType = existingMetadata.returnType();
          Type overridingReturnType = metadata.returnType();
          Type resolvedType = Types.resolveReturnType(existingReturnType, overridingReturnType);
          if (resolvedType.equals(overridingReturnType)) {
            result.put(metadata.configKey(), metadata);
          }
          continue;
        }
        result.put(metadata.configKey(), metadata);
      }
      return new ArrayList<>(result.values());
    }

    /**
     * Called indirectly by {@link #parseAndValidateMetadata(Class)}.
     * @param targetType 当前例子中的 TestClient
     * @param method 当前例子中的 TestClient#getMethods() 中的每个 Method 对象
     */
    protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
      final MethodMetadata data = new MethodMetadata();
      data.targetType(targetType);
      data.method(method);
      data.returnType(
          Types.resolve(targetType, targetType, method.getGenericReturnType()));
      data.configKey(Feign.configKey(targetType, method));
      if (AlwaysEncodeBodyContract.class.isAssignableFrom(this.getClass())) {
        data.alwaysEncodeBody(true);
      }

      if (targetType.getInterfaces().length == 1) {
        // 处理类上的注解
        processAnnotationOnClass(data, targetType.getInterfaces()[0]);
      }
      processAnnotationOnClass(data, targetType);

      // 处理方法上的注解
      for (final Annotation methodAnnotation : method.getAnnotations()) {
        processAnnotationOnMethod(data, methodAnnotation, method);
      }
      if (data.isIgnored()) {
        return data;
      }
      checkState(data.template().method() != null,
          "Method %s not annotated with HTTP method type (ex. GET, POST)%s",
          data.configKey(), data.warnings());

      // 处理参数上的注解
      final Class<?>[] parameterTypes = method.getParameterTypes();
      final Type[] genericParameterTypes = method.getGenericParameterTypes();

      final Annotation[][] parameterAnnotations = method.getParameterAnnotations();
      final int count = parameterAnnotations.length;
      for (int i = 0; i < count; i++) {
        boolean isHttpAnnotation = false;
        if (parameterAnnotations[i] != null) {
          isHttpAnnotation = processAnnotationsOnParameter(data, parameterAnnotations[i], i);
        }

        if (isHttpAnnotation) {
          data.ignoreParamater(i);
        }

        if (parameterTypes[i] == URI.class) {
          data.urlIndex(i);
        } else if (!isHttpAnnotation && parameterTypes[i] != Request.Options.class) {
          if (data.isAlreadyProcessed(i)) {
            checkState(data.formParams().isEmpty() || data.bodyIndex() == null,
                "Body parameters cannot be used with form parameters.%s", data.warnings());
          } else if (!data.alwaysEncodeBody()) {
            checkState(data.formParams().isEmpty(),
                "Body parameters cannot be used with form parameters.%s", data.warnings());
            checkState(data.bodyIndex() == null,
                "Method has too many Body parameters: %s%s", method, data.warnings());
            data.bodyIndex(i);
            data.bodyType(
                Types.resolve(targetType, targetType, genericParameterTypes[i]));
          }
        }
      }

      if (data.headerMapIndex() != null) {
        checkMapString("HeaderMap", parameterTypes[data.headerMapIndex()],
            genericParameterTypes[data.headerMapIndex()]);
      }

      if (data.queryMapIndex() != null) {
        if (Map.class.isAssignableFrom(parameterTypes[data.queryMapIndex()])) {
          checkMapKeys("QueryMap", genericParameterTypes[data.queryMapIndex()]);
        }
      }

      return data;
    }
主要是处理类上的注解,方法上的注解,方法参数上的注解。processAnnotationOnClass,processAnnotationOnMethod,processAnnotationOnClass,可以看到这三个方法参数都会传入MethodMetadata,应该是进行了各种参数保存。这三个抽象类由 DeclarativeContract 实现并且提供了相应的注解和MethodMetadata的对应关系,这样不管是 @RequestLine 还是别的注解(比如解析spring mvc的注解),只要指定了和MethodMetadata对应的关系就可以解析了。
// 拿方法上的注解为例
public abstract class DeclarativeContract extends BaseContract {

 ......

private final List<GuardedAnnotationProcessor> methodAnnotationProcessors = new ArrayList<>();

/**
   * @param data metadata collected so far relating to the current java method.
   * @param annotation annotations present on the current method annotation.
   * @param method method currently being processed.
   */
  @Override
  protected final void processAnnotationOnMethod(MethodMetadata data,
                                                 Annotation annotation,
                                                 Method method) {
    List<GuardedAnnotationProcessor> processors = methodAnnotationProcessors.stream()
        .filter(processor -> processor.test(annotation))
        .collect(Collectors.toList());

    if (!processors.isEmpty()) {
      // 对 methodAnnotationProcessors 中定义的注解和与MethodMetadata的对应关系
      // 一顿循环把注解中的值解析到MethodMetadata中
      processors.forEach(processor -> processor.process(annotation, data));
    } else {
      data.addWarning(String.format(
          "Method %s has an annotation %s that is not used by contract %s",
          method.getName(),
          annotation.annotationType()
              .getSimpleName(),
          getClass().getSimpleName()));
    }
  }

  /**
   * Called while method annotations are being processed
   * 
   * 把 注解 和 对注解解析的结果与MethodMetadata对应关系添加到methodAnnotationProcessors集合中
   *
   * @param annotationType to be processed
   * @param processor function that defines the annotations modifies {@link MethodMetadata}
   */
  protected <E extends Annotation> void registerMethodAnnotation(Class<E> annotationType,
                                                                 DeclarativeContract.AnnotationProcessor<E> processor) {
    registerMethodAnnotation(
        annotation -> annotation.annotationType().equals(annotationType),
        processor);
  }

  /**
   * Called while method annotations are being processed
   *
   * @param predicate to check if the annotation should be processed or not
   * @param processor function that defines the annotations modifies {@link MethodMetadata}
   */
  protected <E extends Annotation> void registerMethodAnnotation(Predicate<E> predicate,
                                                                 DeclarativeContract.AnnotationProcessor<E> processor) {
    this.methodAnnotationProcessors.add(new GuardedAnnotationProcessor(predicate, processor));
  }

  ......
}

? ? ? ? 到这里猜测下只要随便定义注解和注解与MethodMetadata对应关系就可以工作起来了。这里看看在?feign.Feign.Builder 中?Contract 的实现类就知道是哪些注解了?

private Contract contract = new Contract.Default();

? ? ? ? 看看这个 default 实现做了什么。

class Default extends DeclarativeContract {

    static final Pattern REQUEST_LINE_PATTERN = Pattern.compile("^([A-Z]+)[ ]*(.*)$");

    public Default() {
      // 定义各种注解和与MethodMetadata的对应关系
      // λ 表达式就是对应关系的处理
      // λ 表达式第一个参数为前面的注解,第二个参数为 MethodMetadata 实例
      
      // 定义类上的注解
      super.registerClassAnnotation(Headers.class, (header, data) -> {
        final String[] headersOnType = header.value();
        checkState(headersOnType.length > 0, "Headers annotation was empty on type %s.",
            data.configKey());
        final Map<String, Collection<String>> headers = toMap(headersOnType);
        headers.putAll(data.template().headers());
        data.template().headers(null); // to clear
        data.template().headers(headers);
      });

      // 定义方法上的注解
      super.registerMethodAnnotation(RequestLine.class, (ann, data) -> {
        final String requestLine = ann.value();
        checkState(emptyToNull(requestLine) != null,
            "RequestLine annotation was empty on method %s.", data.configKey());

        final Matcher requestLineMatcher = REQUEST_LINE_PATTERN.matcher(requestLine);
        if (!requestLineMatcher.find()) {
          throw new IllegalStateException(String.format(
              "RequestLine annotation didn't start with an HTTP verb on method %s",
              data.configKey()));
        } else {
          data.template().method(HttpMethod.valueOf(requestLineMatcher.group(1)));
          data.template().uri(requestLineMatcher.group(2));
        }
        data.template().decodeSlash(ann.decodeSlash());
        data.template()
            .collectionFormat(ann.collectionFormat());
      });
      super.registerMethodAnnotation(Body.class, (ann, data) -> {
        final String body = ann.value();
        checkState(emptyToNull(body) != null, "Body annotation was empty on method %s.",
            data.configKey());
        if (body.indexOf('{') == -1) {
          data.template().body(body);
        } else {
          data.template().bodyTemplate(body);
        }
      });
      super.registerMethodAnnotation(Headers.class, (header, data) -> {
        final String[] headersOnMethod = header.value();
        checkState(headersOnMethod.length > 0, "Headers annotation was empty on method %s.",
            data.configKey());
        data.template().headers(toMap(headersOnMethod));
      });
      
      // 定义参数上的注解
      super.registerParameterAnnotation(Param.class, (paramAnnotation, data, paramIndex) -> {
        final String annotationName = paramAnnotation.value();
        final Parameter parameter = data.method().getParameters()[paramIndex];
        final String name;
        if (emptyToNull(annotationName) == null && parameter.isNamePresent()) {
          name = parameter.getName();
        } else {
          name = annotationName;
        }
        checkState(emptyToNull(name) != null, "Param annotation was empty on param %s.",
            paramIndex);
        nameParam(data, name, paramIndex);
        final Class<? extends Param.Expander> expander = paramAnnotation.expander();
        if (expander != Param.ToStringExpander.class) {
          data.indexToExpanderClass().put(paramIndex, expander);
        }
        if (!data.template().hasRequestVariable(name)) {
          data.formParams().add(name);
        }
      });
      super.registerParameterAnnotation(QueryMap.class, (queryMap, data, paramIndex) -> {
        checkState(data.queryMapIndex() == null,
            "QueryMap annotation was present on multiple parameters.");
        data.queryMapIndex(paramIndex);
        data.queryMapEncoded(queryMap.encoded());
      });
      super.registerParameterAnnotation(HeaderMap.class, (queryMap, data, paramIndex) -> {
        checkState(data.headerMapIndex() == null,
            "HeaderMap annotation was present on multiple parameters.");
        data.headerMapIndex(paramIndex);
      });
    }

 ......
}

? ? ? ? 熟悉的?@RequestLine 。

Feign 是如何真实的发起 http 请求?

? ? ? ? 调用 TestClient#test() 方法 真正的发起了 http 调用,那么入口在哪??TestClient 的实例是通过 jdk 的动态代理拿到的,这样的话入口应该是?java.lang.reflect.InvocationHandler#invoke() 方法。前面说的?InvocationHandlerFactory (接口代理方法的分发调用)可能是入口。

feign.Feign.Builder中的实现应该是关键。


    private InvocationHandlerFactory invocationHandlerFactory =
        new InvocationHandlerFactory.Default();
  static final class Default implements InvocationHandlerFactory {

    @Override
    public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) {
      return new ReflectiveFeign.FeignInvocationHandler(target, dispatch);
    }
  }
// 实现了java.lang.reflect.InvocationHandler
  static class FeignInvocationHandler implements InvocationHandler {

    private final Target target;
    
    // newInstance()得到的接口方法和方法的处理逻辑
    private final Map<Method, MethodHandler> dispatch;

    FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {
      this.target = checkNotNull(target, "target");
      this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      if ("equals".equals(method.getName())) {
        try {
          Object otherHandler =
              args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
          return equals(otherHandler);
        } catch (IllegalArgumentException e) {
          return false;
        }
      } else if ("hashCode".equals(method.getName())) {
        return hashCode();
      } else if ("toString".equals(method.getName())) {
        return toString();
      }
      
      // 具体的实现委派给了MethodHandler的实现类
      // 这里是 SynchronousMethodHandler
      return dispatch.get(method).invoke(args);
    }

  ......
}

? ? ? ? 终于找到了?java.lang.reflect.InvocationHandler#invoke() 方法,但是真正的逻辑被委派到了?SynchronousMethodHandler#invoke()

final class SynchronousMethodHandler implements MethodHandler {

 ......

@Override
  public Object invoke(Object[] argv) throws Throwable {
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    Options options = findOptions(argv);
    Retryer retryer = this.retryer.clone();
    while (true) {
      try {
        return executeAndDecode(template, options);
      } catch (RetryableException e) {
        try {
          retryer.continueOrPropagate(e);
        } catch (RetryableException th) {
          Throwable cause = th.getCause();
          if (propagationPolicy == UNWRAP && cause != null) {
            throw cause;
          } else {
            throw th;
          }
        }
        if (logLevel != Logger.Level.NONE) {
          logger.logRetry(metadata.configKey(), logLevel);
        }
        continue;
      }
    }
  }

 ......
}

? 就这样当调用 TestClient#test() 方法的时候真正的发起了 http 请求。

简单的总结

? ? ? ? Feign 在 build()?的时候通过定义各种配置组件然后调用?newInstance() ,解析注解上面的值,最终得到一个持有?Map<Method, MethodHandler> dispatch 的 动态代理对象。通过调用动态代理对象的方法来实现 http 请求。这是个猜想+验证的过程。

  开发测试 最新文章
pytest系列——allure之生成测试报告(Wind
某大厂软件测试岗一面笔试题+二面问答题面试
iperf 学习笔记
关于Python中使用selenium八大定位方法
【软件测试】为什么提升不了?8年测试总结再
软件测试复习
PHP笔记-Smarty模板引擎的使用
C++Test使用入门
【Java】单元测试
Net core 3.x 获取客户端地址
上一篇文章      下一篇文章      查看所有文章
加:2021-09-10 11:09:24  更:2021-09-10 11:09:54 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/21 4:47:55-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码