专栏首页BAT的乌托邦[享学Feign] 三、原生Feign的核心API详解(一):UriTemplate、HardCodedTarget...

[享学Feign] 三、原生Feign的核心API详解(一):UriTemplate、HardCodedTarget...

美国为什么敢打伊拉克?因为怀疑他有大规模杀伤性武器;美国为什么不敢打俄罗斯?因为他真的有大规模杀伤性武器。战场上,请用实力说话

代码下载地址:https://github.com/f641385712/feign-learning

前言

前两篇文章站在使用的角度介绍了源生Feign,相信读过的话都知道如何使用了。那么接下来就要动动真格,扒开内裤看看里面到底是什么东西。

本文将着重了解Feign的核心API,有点像啃API、啃源码的意思,所以可能相对枯燥、难懂,所以需要坚持。因为学习起来虽然枯燥,但意义却是巨大的,正所谓“上天不会亏待努力的人,也不会同情假勤奋的人”。


正文

核心API的讲解,将按照自上而下,有内向外的“方向”一一描述。


核心API

注解的使用起来门槛低且简单,这是元编程的最大优势。但作为一位专业认识,要真正掌握它还得深入,这就是Feign的核心API部分,它是一切高级使用、定制的基石。


Template

它用于表示:按照 RFC 6570 标准书写的表达式模版,有了此模版后续就可以用变量进行替换形成最终值。

说明:模版字符串遵循的标准是RFC 6570,各位可做简单的学习了解,不用太深。(Spring MVC遵循的是Ant风格的模版,理解起来貌似更容易些…)

public class Template {

	// 唯一构造器,访问权限是default
	Template( ... ) { ... }
	
	// 填充参数 表达式可以是个feign.template.Expression对象
	// 一个{}就是一个Expression对象,根据name(key)进行匹配
	public String expand(Map<String, ?> variables) { ... }
	protected String resolveExpression(Expression expression, Map<String, ?> variables) { ... }
	
	// Uri Encode  URI编码
	// encodeSlash true的话连/也会给转义,默认不转义它
	private String encode(String value) { ... }

	// 拿到当前template下所有的变量的**名称**
	public List<String> getVariables() { ... }
}

该类并不是抽象类,并且它还有4个子类,分别对应着可以使用模版语法的几个注解


UriTemplate

顾名思义,它是用于处理URI的模版,用于处理@RequestLine的模版

public class UriTemplate extends Template {

	public static UriTemplate create(String template, Charset charset) {
	  return new UriTemplate(template, true, charset);
	}
	... // 省略其它构造器
}

本类源码非常简单,并没有在父类基础做扩展,仅提供了构建实例的静态方法(因为父类构造器是default权限的)。

使用示例

@Test
public void fun1() {
    UriTemplate template = UriTemplate.create("http://example.com/{foo}", StandardCharsets.UTF_8);
    Map<String, Object> params = new HashMap<>();
    params.put("foo", "bar");
    String result = template.expand(params);
    System.out.println(result);

    System.out.println("=======================================");

    // 对斜杠不要转义
    template = UriTemplate.create("http://example.com/{empty}{foo}index.html{frag}",false, StandardCharsets.UTF_8);

    params.clear();
    // params.put("empty",null);
    params.put("foo","houses/");
    params.put("frag","?g=sec1.2");
    result = template.expand(params);
    System.out.println(result);
}

运行程序,输出:

http://example.com/bar
=======================================
http://example.com/houses/index.html%3Fg=sec1.2

小细节:/没被转义,但?被转义为了%3,可见默认情况下它会对URI上的特殊符号均进行转义;另外,{empty}因为没有给值(效果同赋值为null),所以根本就没出现在URI里

说明:这是模版处理的一个很大的特点 --> 若对应key不存在或值为null,那么此部分表达式将被忽略 需要注意的是:若值的空串,那是表示有值,空串而已嘛,还是有意义的

我找了一个关于RFC6570语法格式的在线测试工具:http://www.utilities-online.info/uritemplate,你可以试试,熟悉熟悉feign的URL模版语法。


QueryTemplate

查询参数(Query String parameter)的模版。用于处理@QueryMap的模版

public final class QueryTemplate extends Template {

	// 这里name没有用字符串而是使用了模版类型,是因为name也可以是个模版{}
	// 大部分情况下它可以是字符串即可
	private final Template name;
	// 因为一个key可以对应多值,所以用List肯定没错喽
	private List<String> values; 
  
  	// 当一key多值时候用什么分隔符,支持:, \t | 等等
  	// 默认它是CollectionFormat.EXPLODED,也就是会以foo=bar&foo=baz这种形式拼接起来
  	private final CollectionFormat collectionFormat;

	// 请注意:默认这里使用的是CollectionFormat.EXPLODED哦
  	public static QueryTemplate create(String name, Iterable<String> values, Charset charset) {
    	return create(name, values, charset, CollectionFormat.EXPLODED);
  	}
	... // 省略append等其它方法
	
	public List<String> getValues() {
	  return values;
	}
	public String getName() {
	  return name.toString();
	}

  	// 可以看到它expand实际上是对name进行expand而已啦~~~~~
	@Override
	public String expand(Map<String, ?> variables) {
	  String name = this.name.expand(variables);
	  return this.queryString(name, super.expand(variables));
	}
  	...
}

该模版用于构造一个查询参数,一次性构建一个,若有多个key需要构建多次。

使用示例

@Test
public void fun2() {
	// 可以看到key也是可以使用模版的。当然你也可以直接使用字符串即可,也可以混合使用
    QueryTemplate template = QueryTemplate.create("hobby-{arg}", Arrays.asList("basket", "foot"), StandardCharsets.UTF_8);
    Map<String, Object> params = new HashMap<>();
    // params.put("arg", "1");

    String result = template.expand(params);
    System.out.println(result);

    template = QueryTemplate.create("grade", Arrays.asList("1", "2"), StandardCharsets.UTF_8, CollectionFormat.CSV);
    System.out.println(template);
}

输出:

hobby-%7Barg%7D=basket&hobby-%7Barg%7D=foot
grade=1,2

注意:这里arg必传,不能给null,否则会输出:hobby-%7Barg%7D=basket&hobby-%7Barg%7D=foot原样输出了,不会忽略内部表达式的。


HeaderTemplate

构造请求头的模版,几乎同上,用于处理@QueryMap的模版,本处省略。


BodyTemplate

用于表示标注有@Body注解的模版。

public final class BodyTemplate extends Template {

  // 虽不强制必须是Json,但对它进行了特殊支持
  private static final String JSON_TOKEN_START = "{";
  private static final String JSON_TOKEN_END = "}";
  private static final String JSON_TOKEN_START_ENCODED = "%7B";
  private static final String JSON_TOKEN_END_ENCODED = "%7D";
  private boolean json = false;

  public static BodyTemplate create(String template) {
     return new BodyTemplate(template, Util.UTF_8);
  }
  private BodyTemplate(String value, Charset charset) {
     super(value, ExpansionOptions.ALLOW_UNRESOLVED, EncodingOptions.NOT_REQUIRED, false, charset);
     // 判断是否是Json。如果你的模版字符串是以%7B打头%7D结尾的,就标记是JSON,后面填充时会特殊处理
     // 说明:这个自己手动构造构造不出来的,只有通过编码器处理过才有可能这里是true
     if (value.startsWith(JSON_TOKEN_START_ENCODED) && value.endsWith(JSON_TOKEN_END_ENCODED)) {
      this.json = true;
    }
  }

  // 若是JSON,会进行特殊处理
  @Override
  public String expand(Map<String, ?> variables) {
    String expanded = super.expand(variables);
    if (this.json) {
      /* decode only the first and last character */
      StringBuilder sb = new StringBuilder();
      sb.append(JSON_TOKEN_START);
      sb.append(expanded,
          expanded.indexOf(JSON_TOKEN_START_ENCODED) + JSON_TOKEN_START_ENCODED.length(),
          expanded.lastIndexOf(JSON_TOKEN_END_ENCODED));
      sb.append(JSON_TOKEN_END);
      return sb.toString();
    }
    return expanded;
  }
}

该模版唯一的特点便是:对JSON格式进行了兼容处理,没啥太多好说的。

使用示例

@Test
public void fun3(){
    BodyTemplate template = BodyTemplate.create("data:{body}");

    Map<String, Object> params = new HashMap<>();
    params.put("body", "{\"name\": \"YourBatman\",\"age\": 18}");

    String result = template.expand(params);
    System.out.println(result);
}

输出:data:{"name": "YourBatman","age": 18}


Target

它作用类似于JAXRS里面的javax.ws.rs.client.WebTarget这个类,用于把请求模版RequestTemplate 转换为实际请求实例 feign.Request,此Request后续交给Client发送Http请求。

// 它是个泛型接口
public interface Target<T> {

  // 此target作用的接口的类型
  Class<T> type();
  // 此target配置的一个key,一般情况下没啥用
  // 但会在和feign-hystrix整合时,会作为它的groupKey来使用,这也是它的唯一被使用的地方
  String name();
  // 发送请求的Base URL。如:https://example/api/v1
  String url();

  // 最重要的方法:用于把请求模版组装、加上Base Url、转换为一个真正的Request
  // 此input模版里包含有很多的:QueryTemplate/HeaderTemplate/UriTemplate等等等等
  Request apply(RequestTemplate input);
}

该接口有两个实现类:

说明:Feign的源码设计上存在一大特点,偏向于把接口默认实现类以及相关类以静态内部类的形式内聚在一起,所以它的代码内聚性是非常之高的。


EmptyTarget

顾名思义,它就是“啥都不做”,它的特点是木有Base URL。

public static final class EmptyTarget<T> implements Target<T> {
	
	// 说明:它并不需要URL属性
	private final Class<T> type;
    private final String name;

    public static <T> EmptyTarget<T> create(Class<T> type) {
      return new EmptyTarget<T>(type, "empty:" + type.getSimpleName());
    }
    ...
    // 它并不需要URL,因为不需要做啥
    @Override
    public String url() {
      throw new UnsupportedOperationException("Empty targets don't have URLs");
    }

	// 如果请求模版的URL里已经包含了http,也就是说是绝对路径了,那就抛错
	// input.request() -> Request.create(this.method, this.url(), this.headers(), this.requestBody())
	// 此处的this.url()就是个相对路径喽。比如/api/v1/demo这种
    @Override
    public Request apply(RequestTemplate input) {
      if (input.url().indexOf("http") != 0) {
        throw new UnsupportedOperationException("Request with non-absolute URL not supported with empty target");
      }
      return input.request();
    }
	...
}

它最大的特点就是不能有Base Url,所以就好似什么都没做一样


HardCodedTarget

硬编码目标类。它的三大参数都不能为null

public static class HardCodedTarget<T> implements Target<T> {

    private final Class<T> type;
    private final String name;
    private final String url; // 相较于Empty,它必须有url

	...
	// 不同于其它的,它的实例是通过构造器创建的
    public HardCodedTarget(Class<T> type, String name, String url) {
      this.type = checkNotNull(type, "type");
      this.name = checkNotNull(emptyToNull(name), "name");
      this.url = checkNotNull(emptyToNull(url), "url");
    }
    ... // 省略3个get方法

	// 如果请求模版的URL不含有http,也就是说是个相对路径,这里就把Base Url加进去
	// 如果请求模版已经是绝对路径了,那就不管啦
	@Override
    public Request apply(RequestTemplate input) {
      if (input.url().indexOf("http") != 0) {
        input.target(url());
      }
      return input.request();
    }
}

该实现类是常用的、缺省的,比如feign.Feign类就是用它来代理目标接口类的,需要掌握。


Client

顾名思义,它就是用来发送feign.Request这个Http请求的,请注意:实现此接口的子类实现请确保是线程安全的(因为可能是多线程发送)

public interface Client {
	// Options是一些选项参数,比如:
	// connectTimeoutMillis链接超时时间,默认是:10s
	// readTimeoutMillis链接超时时间,默认是:60s
	Response execute(Request request, Options options) throws IOException;
}

该接口简单:有且仅有这一个方法表示执行http请求。有如下两个实现类:


Default

作为Client接口的默认实现,它是基于JDK的HttpURLConnection来发送Http请求的。feign.Feign默认情况下就是使用它来完成http请求的发送。

class Default implements Client {

	// 这些类都是java.net包下的
    private final SSLSocketFactory sslContextFactory;
    private final HostnameVerifier hostnameVerifier;

	// 发送Request 请求 -> 最终通过HttpURLConnection 完成的最终发送
    @Override
    public Response execute(Request request, Options options) throws IOException {
     // 通过feign.Request构建出一个HttpURLConnection 
      HttpURLConnection connection = convertAndSend(request, options);
      // 得到Response,交给后面去解析
      return convertResponse(connection, request);
    }
	...
}

这是Feign缺省的发送请求的方式:底层使用JDK源生的HttpURLConnection。对于JDK源生的这个Client,请务必注意它自动把GET给你转为POST的场景,避免踩坑哦。


Proxied

支持到JDK的java.net.Proxy(比如访问外国网站),使用极少,略。

说明:此处指的是java.net.Proxy,而不是java.lang.reflect.Proxy


Retryer

重试器。它能对每次执行Http请求Client#execute()的状态克隆保持,从而根据配置决定是否应该重试。

public interface Retryer extends Cloneable {

  // 如果允许重试,则return返回(可能在睡眠后)。
  // 否则会把这个异常继续传播
  void continueOrPropagate(RetryableException e);
 
  @Override
  Retryer clone();

 	// 永不重试实例
 	// 生产环境下:建议使用永不重试,否则请做好幂等
  Retryer NEVER_RETRY = new Retryer() {
    @Override
    public void continueOrPropagate(RetryableException e) {
      throw e;
    }
    @Override
    public Retryer clone() {
      return this;
    }
  };
}

内置的有且仅有一个实现:feign.Retryer.Default


Default

Feign默认使用的是Default这个实例,而不是NEVER_RETRY永不重试哦。

class Default implements Retryer {
	
	// 重试参数
    private final int maxAttempts;
    private final long period;
    private final long maxPeriod;
	
	// 内部统计指标计数
    int attempt; //计数:重试了几次
    long sleptForMillis; // 一共休息了多久

	// 空构造,默认重试参数
	// period:默认100ms重试一次
	// maxPeriod:最大重试1秒钟
	// maxAttempts:最多重试5次
	// 
    public Default() {
      this(100, SECONDS.toMillis(1), 5);
    }
    ...

	@Override
    public void continueOrPropagate(RetryableException e) {
     // 若超过了最大重试次数,异常继续向上抛出
      if (attempt++ >= maxAttempts) {
        throw e;
      }
      // ... // 一套重试逻辑
	}
	
	// 注意:此处克隆就是new一个新的实例供以使用(配置完全一样喽)
    @Override
    public Retryer clone() {
      return new Default(period, maxPeriod, maxAttempts);
    }
}

因此注意注意注意:默认情况下,Feign是有重试机制的,并且是100ms重试一次,默认重试5次

说明:很多小伙伴跌到在这里,生产环境下我建议你务必关闭Feign的重试机制,避免不必要的麻烦


总结

Feign的核心API内容不少,第一部分就讲解到这了。

Template负责对模版语法的解析、Target代理着接口并且把RequestTemplate转为Request、Client负责把Request通过Http请求发送出去、Retryer负责失败重试逻辑。还有请求具体发送流程,它是通过MethodHandler去处理的,这在下篇文章会详细介绍。

声明

原创不易,码字不易,多谢你的点赞、收藏、关注。把本文分享到你的朋友圈是被允许的,但拒绝抄袭

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • [享学Netflix] 五、Apache Commons Configuration2.x别样的Builder模式:ConfigurationBuilder

    代码下载地址:https://github.com/f641385712/netflix-learning

    YourBatman
  • [享学Netflix] 十、Archaius对Commons Configuration核心API Configuration的扩展实现

    上篇文章体验了一把Netflix Archaius的使用,感受到了它对配置管理上的便捷性。或许有小伙伴会说,配置管理上它和Apache Commons Conf...

    YourBatman
  • 【小家Spring】Spring中读取配置的方式,@Value、@PropertySource、@ConfigurationProperties使用详解

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

    YourBatman
  • springBoot系列教程06:参数验证及验证信息国际化

    在springboot应用中要验证参数是否正确很简单,web应用已经包含了validation的

    肖哥哥
  • 【基础】通过反射访问private成员和方法,private设计的

    注意代码中的通过dm.setAccessible(true)这样在调用方法时他不会检查方法的修饰是public还是private。如果没有dm.setAcces...

    用户5640963
  • 快速学习-JPA中的一对多

    在一对多关系中,我们习惯把一的一方称之为主表,把多的一方称之为从表。在数据库中建立一对多的关系,需要使用数据库的外键约束。

    cwl_java
  • Java常用的设计模式

    Creator:创建器,声明工厂方法,工厂方法通常会返回一个Product类的实例对象,而且多是抽象方法

    MiChong
  • 详解RxJava2 Retrofit2 网络框架简洁轻便封装

    RxJava2、Retrofit2火了有一段时间了,前段时间给公司的项目引入了这方面相关的技术,在此记录一下相关封装的思路。

    砸漏
  • lombok系列1:初识lombok

    连官网都懒得废话,只给出了一句广告词:给你的java加点料(spice up your java)。

    pollyduan
  • java 利用JAX-RS快速开发RESTful 服务

    JAX-RS(Java API for RESTful Web Services)同样也是JSR的一部分,详细规范定义见 https://jcp.org/en/...

    菩提树下的杨过

扫码关注云+社区

领取腾讯云代金券