OkHttp的高级封装Feign学习(一): Feign注解的使用

说明

在项目开发中,避免不了通过HTTP请求进行对第三方服务的调用,在之前的两遍博文《OkHttp使用踩坑记录总结(一):OkHttpClient单例和长连接Connection Keep-Alive
《OkHttp使用踩坑记录总结(二):OkHttp同步异步请求和连接池线程池》中,我对OkHttp使用过程中遇到的一些问题进行了总结记录。在微服务架构体系中,我们通常使用netflix开源的springcloud组件,在其中通过feign进行服务间的路由调用。本篇博文将学习总结OpenFeign的开源项目feign,学习如何通过feign进行http请求。

正文

OpenFeign是什么?与netflix.feign的区别?

feign是一种java的http客户端,它方便了为REST或SOAP服务编写java客户端,并且可定制化。通过少量代码和开销进行http请求。在其官网介绍了很多特性,以下是其主要开发目标:

  1. 响应缓存,支持api的响应缓存,允许用户根据需要进行选择设置。
  2. 完整的URI Template表达式的支持
  3. 日志Logger api的重构
  4. 重试Retry api的重构
  5. 追踪统计Metric api的支持,使用户能够完整的监控request/response的声明周期
  6. 通过CompletableFuture实现异步执行的支持
  7. 通过Reactive Streams实现响应式执行的支持
  8. 断路器的支持

在微服务中我们使用netflix feign进行服务调用,该项目是netflix开源的springcloud框架项目spring cloud netflix体系的一部分。不过netflix feign是旧的项目名称,已被弃用,移到了新的仓库OpenFeign feign中。之前我们maven使用的依赖为spring-cloud-starter-feign,现已改为spring-cloud-starter-openfeign。

在spring官方文档的介绍中可以看到,feign是一种声明式的web服务客户端,springcloud通过自动装配,绑定到spring环境等方式将OpenFeign集成到了springboot程序中。并且为其添加了springmvc的注解支持,支持springweb中默认使用的HttpMessageCoverters。而且在使用时,集成了Eureka, Spring Cloud LoadBalancer进行负载均衡。

通过以上介绍,我们在项目中使用springcloud时,可以通过spring-cloud-starter-openfeign进行服务间的调用,若对外的第三方服务调用,可以单独使用OpenFeign对OkHttp的高级封装feign-okhttp。

基本使用方式

feign的使用方式,通过创建接口并且使用注解。其原理是将注解处理为模板化请求。
创建项目引入依赖,这里使用feign-okhttp

<dependency>
	<groupId>io.github.openfeign</groupId>
	<artifactId>feign-okhttp</artifactId>
	<version>10.7.4</version>
</dependency>

基本方式,遵循restful风格:

public interface HelloService {
    @RequestLine("GET /test/hello/{name}")
    String hello(@Param("name") String name);
}
HelloService service = Feign.builder()
			.client(new OkHttpClient())
			.target(HelloService.class, "http://localhost:8080/");

注意,feign使用restful风格进行请求时,服务端的api也必须是restful风格,否则会报错

以上的示例中,我们创建了HelloService接口,在hello方法上使用了@RequestLine和@Param注解,测试时通过feign builder创建了对应的客户端。

可以看到,feign的使用方式十分简单 – 接口方法声明,使用注解,对应feign客户端创建,调用请求。

feign支持了6种注解,分别为: @RequestLine @Param @Headers @QueryMap @HeaderMap @Body。接下来,分别介绍注解的使用方式:

@RequestLine

该注解使用在接口的方法上,定义了http请求方法和UriTemplate,其中参数表达式必须在花括号{}中,该表达式将解析方法中使用@Param注解标记的参数

如以上示例中的 @RequestLine(“GET /test/hello/{name}”) 将解析@Param注解中与之对应的name参数值。

在以上示例中创建feign客户端时,已经设置了uri,如果需要为不同的请求设置不同的uri,则可以重写Request Line,在方法中加入一个 java.net.URI 参数。如:

@RequestLine("GET /test/hello/{name}")
String hello(URI host, @Param("name") String name);

feign使用的表达式是URI Template - RFC 6570中定义的简单字符串表达式,可以设置简单的解析规则,如{name:[a-zA-Z]*}。并且在解析值时,遵循以下规则:

  • 无法解析的表达式将被忽略。
  • 所有的值在没有指明编码时,将会使用pct编码。

在feign解析表达式时,首先会判断参数是否被定义,若存在参数定义且值不为null,则查询时参数会得到保留,否则将会忽略表达式。对于未定义的参数和参数值为空的情况,以下是解析结果的示例:

定义方法:

@RequestLine("GET /test/hello2")
ResultPojo hello2(@QueryMap Map<String, Object> querymap);

参数值为空:

Map<String, Object> map = new HashMap<>();
map.put("name", "");

url: http://localhost:8080/test/hello2?name=

参数值为null或不存在参数定义

Map<String, Object> map = new HashMap<>();
map.put("name", null);
or
Map<String, Object> map = new HashMap<>();

url: http://localhost:8080/test/hello2

@Param & @QueryMap

这两个注解都是作用于方法参数上的,@Param注解定义了表达式的参数,表达式解析时,将根据该注解指定的参数名称进行对应解析;@QueryMap定义了键值对形式的参数map或者是简单对象pojo类型的参数。

之前提到如果使用的是restful风格的url,对应的服务端风格也应该一致,否则将会保错。服务端不是restful风格时,我们可以使用以下方式声明方法:

@RequestLine("GET /test/hello?name={name}")
String hello(URI host, @Param("name") String name);
@RequestLine("GET /test/hello2")
String hello2(@QueryMap Map<String, Object> querymap);
@RequestLine("GET /test/hello2")
String hello3(@QueryMap ParamPojo paramPojo);

在@Param注解中,有一个可选的属性expander,通过该属性定义的类来处理参数值。注意,该属性必须是实现Expander接口的类的类对象。expander处理的规则和表达式的规则一致。

示例:发送一个格式为yyyy-MM-dd HH:mm:ss的时间参数
创建DateExpander:

public class DateExpander implements Param.Expander {
    @Override
    public String expand(Object o) {
        LocalDateTime date = (LocalDateTime) o;
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        return date.format(formatter);
    }
}

声明方法:

@RequestLine("GET /test/param/expander?since={date}")
String paramExpanderTest(@Param(value = "date", expander = DateExpander.class)LocalDateTime time);

在使用@ParamMap设置参数时,可以设置Map<String, Object> map 或者 pojo对象。在没有指定特定的QueryMapEncoder时,map的key,pojo的字段名称将会被作为参数名称出现在url中。同时,如果对应的参数值为null,则会忽略该参数。

注意,在使用pojo对象时,如果没有指定encoder,默认使用反射来得到字段名称和值,如果希望使用set和get方法,则要指定使用BeanQueryMapEncoder。

@Headers & @HeaderMap

这两个注解都是用来设置请求头的参数,不同的是@Header注解可以作用于方法或接口上,而@HeaderMap作用于参数上。

@Headers定义了UriTemplate的变体HeaderTempalte,其中的表达式也会根据@Param对应的参数名称进行解析。当它作用于接口上时,这个模板将会用于每个请求,作用于方法上时,就只会影响该方法的请求

@HeaderMap定义了键值对形式的请求头参数。当无法确定请求头的参数名称或使用自定义请求头时,可以使用该注解。

它们的解析规则跟上述一致。

示例:

public interface ContentService {
  @RequestLine("GET /api/documents/{contentType}")
  @Headers("Accept: {contentType}")
  String getDocumentByType(@Param("contentType") String type);
}

注意: 当所有的表达式都有相同的名称时,不管是在@RequestLine @QueryMap @BodyTemplate @Headers中,都会解析相同的值。在以上示例中@RequestLine和@Headers都会解析contentType的值。

@Body

该注解作用于方法上,其定义了一个类似于UriTemplate和HeaderTemplate的Template。其中的表达式也使用了@Param注解中的值进行解析。

Body template遵循以下规则:

  • 无法解析的表达式将会被省略
  • 在设置到请求体前 值 不会通过Encoder进行编码
  • 必须设置Content-Type请求头参数

示例:

@RequestLine("POST /test/post")
@Headers("Content-Type: application/json")
@Body("%7B\"username\": \"{username}\", \"password\": \"{password}\"%7D")
String postTest(@Param("username") String username, @Param("password") String password);

至此,关于feign的注解的学习告一段落。接下来我将继续学习总结feign的其他用法。
其他特性及高级用法请看下篇博文《OpenFeign学习(二):高级用法自定义配置组件HttpClient / SLF4J / RequestInterceptor等》


参考资料:
https://www.baeldung.com/intro-to-feign
https://github.com/OpenFeign/feign
https://stackoverflow.com/questions/49823158/differences-between-netflix-feign-openfeign
https://cloud.spring.io/spring-cloud-openfeign/reference/html/#netflix-feign-starter

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章