在项目开发中,避免不了通过HTTP请求进行对第三方服务的调用,在之前的两遍博文《OkHttp使用踩坑记录总结(一):OkHttpClient单例和长连接Connection Keep-Alive
》和《OkHttp使用踩坑记录总结(二):OkHttp同步异步请求和连接池线程池》中,我对OkHttp使用过程中遇到的一些问题进行了总结记录。在微服务架构体系中,我们通常使用netflix开源的springcloud组件,在其中通过feign进行服务间的路由调用。本篇博文将学习总结OpenFeign的开源项目feign,学习如何通过feign进行http请求。
feign是一种java的http客户端,它方便了为REST或SOAP服务编写java客户端,并且可定制化。通过少量代码和开销进行http请求。在其官网介绍了很多特性,以下是其主要开发目标:
在微服务中我们使用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
io.github.openfeign
feign-okhttp
10.7.4
基本方式,遵循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。接下来,分别介绍注解的使用方式:
该注解使用在接口的方法上,定义了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]*}。并且在解析值时,遵循以下规则:
在feign解析表达式时,首先会判断参数是否被定义,若存在参数定义且值不为null,则查询时参数会得到保留,否则将会忽略表达式。对于未定义的参数和参数值为空的情况,以下是解析结果的示例:
定义方法:
@RequestLine("GET /test/hello2")
ResultPojo hello2(@QueryMap Map querymap);
参数值为空:
Map map = new HashMap<>();
map.put("name", "");
url: http://localhost:8080/test/hello2?name=
参数值为null或不存在参数定义
Map map = new HashMap<>();
map.put("name", null);
or
Map map = new HashMap<>();
url: http://localhost:8080/test/hello2
这两个注解都是作用于方法参数上的,@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 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
注意,在使用pojo对象时,如果没有指定encoder,默认使用反射来得到字段名称和值,如果希望使用set和get方法,则要指定使用BeanQueryMapEncoder。
这两个注解都是用来设置请求头的参数,不同的是@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的值。
该注解作用于方法上,其定义了一个类似于UriTemplate和HeaderTemplate的Template。其中的表达式也使用了@Param注解中的值进行解析。
Body template遵循以下规则:
示例:
@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