概述
Feign是一种声明式、模板化的HTTP Client,目标是使编写Java HTTP Client变得更简单。Feign通过使用Jersey和CXF等工具实现一个HTTP Client,用于构建REST或SOAP的服务。Feign还支持用户基于常用的HTTP工具包(OkHTTP、HTTPComponents)实现自定义的HTTP Client。
Feign基于@EnableFeignClients注解的方式将HTTP请求模板化。Feign将HTTP请求参数写入Template,极大地简化HTTP请求。提供请求回放功能,使HTTP单元测试变得更加方便。
Feign应用一般依赖服务发现组件来实现远程接口调用,在并发要求不高的情况下可以作为RPC方案使用,实现服务之间的解耦。
整合Ribbon和Hystrix,从而不再需要显式地使用这两个组件。Feign还提供HTTP请求的模板,通过编写简单的接口和插入注解,就可以定义好HTTP请求的参数、格式、地址等信息。Feign会完全代理HTTP的请求,只需要像调用方法一样调用它就可以完成服务请求。
Feign特性:
- 可插拔的注解支持,包括Feign注解和JAX-RS注解
- 支持可插拔的HTTP编码器和解码器
- 支持Hystrix和它的Fallback
- 支持Ribbon的负载均衡
- 支持HTTP请求和响应的压缩
常用注解
原理
OpenFeign使用动态代理来封装远程服务调用的过程
步骤 1 到 3 是在项目启动阶段加载完成的,第 4 步调用远程服务是发生在项目的运行阶段。
几个关键步骤:
- 在项目启动阶段,OpenFeign 框架会发起一个主动的扫包流程,从指定的目录下扫描并加载所有被 @FeignClient 注解修饰的接口
- OpenFeign 会针对每一个 FeignClient 接口生成一个动态代理对象,即FeignProxyService,在继承关系上属于 FeignClient 注解所修饰的接口的实例
- 这个动态代理对象会被添加到 Spring 上下文中,并注入到对应的服务里,即LocalService 服务
- LocalService 会发起底层方法调用。实际上这个方法调用会被 OpenFeign 生成的代理对象接管,由代理对象发起一个远程服务调用,并将调用的结果返回给LocalService。
OpenFeign 组件加载过程:
- 项目加载:在项目启动阶段,EnableFeignClients 注解扮演启动开关角色,它使用 Spring 框架的 Import 注解导入 FeignClientsRegistrar 类,开始OpenFeign 组件的加载过程。
- 扫包:FeignClientsRegistrar 负责 FeignClient 接口的加载,它会在指定的包路径下扫描所有的 FeignClients 类,并构造 FeignClientFactoryBean 对象来解析FeignClient 接口。
- 解析 FeignClient 注解:FeignClientFactoryBean功能,解析FeignClient 接口中的请求路径和降级函数的配置信息;触发动态代理的构造过程。其中,动态代理构造是由更下一层的 ReflectiveFeign 完成的。
- 构建动态代理对象:ReflectiveFeign 包含OpenFeign 动态代理的核心逻辑,它主要负责创建出 FeignClient 接口的动态代理对象。ReflectiveFeign 在这个过程中有两个重要任务,一个是解析 FeignClient 接口上各个方法级别的注解,将其中的远程接口URL、接口方法类型、各个请求参数等封装成元数据,并为每一个方法生成一个对应的 MethodHandler 类作为方法级别的代理;另一个重要任务是将这些MethodHandler 方法代理做进一步封装,通过 Java 标准的动态代理协议,构建一个实现 InvocationHandler 接口的动态代理对象,并将这个动态代理对象绑定到FeignClient 接口上。这样一来,所有发生在 FeignClient 接口上的调用,最终都会由它背后的动态代理对象来承接。
MethodHandler 的构建过程涉及到复杂的元数据解析,OpenFeign 组件将FeignClient 接口上的各种注解封装成元数据,并利用这些元数据把一个方法调用翻译成一个远程调用的 Request 请求。
元数据解析,依赖于 OpenFeign 组件中的Contract协议解析功能。Contract 是最顶层抽象接口,实现类如SpringMvcContract,专门用于解析 Spring MVC 标签。
实战
通过Feign上传MultipartFile
现有FS服务,有一Controller层接口:
@PostMapping(value = "/ossUploadPrivateFile")
public Response<UploadFileVO> ossUploadPrivateFile(@RequestPart(value = "file") MultipartFile multipartFile) {
}
微服务开发模式下,其他服务想要使用FS服务提供的接口,则FS服务需提供一个即jar包,即新增一个fs-client module,新增Feign接口:
@FeignClient(value = "fs-provider", configuration = FeignMultipartSupportConfig.class)
public interface RemoteFileService {
/**
* 上传文件
*/
@PostMapping(value = "/fs/ossUploadFile")
Response<UploadFileVO> upload(@RequestPart(value = "file") MultipartFile multipartFile);
}
其他服务在使用FS-client时,启动报错:
Type definition error: [simple type, class java.io.FileDescriptor]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
No serializer found for class java.io.FileDescriptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)
(through reference chain: org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile[\"inputStream\"]->java.io.FileInputStream[\"fd\"])
添加如下依赖:
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form-spring</artifactId>
</dependency>
配置类:
@Configuration
public class FeignMultipartSupportConfig {
@Bean
@Primary
@Scope("prototype")
public Encoder multipartFormEncoder() {
return new SpringFormEncoder();
}
}
然后在RemoteFileService
的@FeignClient
指定上述配置。
feign.FeignException: status 404 reading
报错日志:
status 404 reading RemotePaymentService#queryIsPay(String,String)
feign.FeignException: status 404 reading RemotePaymentService#queryIsPay(String,String)
at feign.FeignException.errorStatus(FeignException.java:78)
at feign.codec.ErrorDecoder$Default.decode(ErrorDecoder.java:93)
at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:149)
at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:78)
at feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:103)
at com.aba.enduser.controller.UserBenefitController.getUnionUserBenefit(UserBenefitController.java)
enduser
服务调用payment
服务提供的Feign接口:
@FeignClient(name = "payment-provider", configuration = FeignConfig.class)
public interface RemotePaymentService {
@RequestMapping(value = {"/pay/queryIsPay/{channel}/{userId}"}, method = {RequestMethod.GET})
Boolean queryIsPay(@PathVariable(name = "channel") String channel, @PathVariable(value = "userId") String userId);
}
payment
服务已先于enduser
服务打包发布。打包会将payment-client
,即RemotePaymentService
所在的jar包部署到私服nexus,发布则是将payment-provider
,将Feign接口对应的Controller层接口注册到Consul。enduser请求payment服务,不应该出现404报错的啊。
迷思,困惑,排查。。
好在有SkyWalking分布式调用链工具,拿到报错日志的TraceId,在ELK里搜索,发现点猫腻:
enduser服务请求payment时,未传参channel
。payment提供的接口是/pay/queryIsPay/aa/bb
,并没有提供接口/pay/queryIsPay//bb
,或/pay/queryIsPay/aa/
,或/pay/queryIsPay//
。
所以,理所当然报错404。
另外,在这个TraceId调用链里,再次看到熟悉的No message available
,参考文末的链接。
反思
先看一下接口定义:
public @interface PathVariable {
/**
* Whether the path variable is required.
* Defaults to true, leading to an exception being thrown if the path
* variable is missing in the incoming request. Switch this to false if
* you prefer a {@code null} or Java 8 {@code java.util.Optional} in this case.
* e.g. on a {@code ModelAttribute} method which serves for different requests.
*/
boolean required() default true;
}
上面提到的Feign接口,并没有显示标注required = true
,因为是默认值。
请求参数缺失,为啥没有报错呢??
问题
反序列化
参考链接见文末,简单总结下:
- 如果Controller层定义的接口:
@PostMapping(value = "/initialChannelPayGoodsList")
public Response<Boolean> initialChannelPayGoodsList(@RequestBody String channel) {
return Response.success(Boolean.TRUE);
}
- Feign里定义的接口:
@RequestMapping(value = "/pay/initialChannelPayGoodsList", method = {RequestMethod.POST})
Boolean initialChannelPayGoodsList(@RequestBody String channel);
两个地方的接口返回类型不一致,就会出现反序列化问题。
@RequestParam
& @PathVariable
@RequestParam
:用于将方法的参数与Web请求里传递的参数进行绑定。
@PathVariable
:将方法中的参数绑定到请求URI中的模板变量上。可以通过@RequestMapping注解来指定URI的模板变量,然后使用@PathVariable注解将方法中的参数绑定到模板变量上。允许使用value或name属性来给参数取一个别名。
区别:对于一个请求:https://api.com/api/user/123?name=johnny
,@PathVariable
可绑定到userId=123
,@RequestParam
则用于获取name=johnny
。
但是在微服务开发中,不建议使用@PathVariable,两个原因:
- 404问题,上面已经提到。看到ELK里报错404,不易排查具体是哪个参数缺失。虽然可以借助于SkyWalking这类工具进行排查。但还是会花费一定时间。
- 不利于统计。
@RequestParam
和@PathVariable
都可以实现业务开发编码需求,但coding应该只是程序员工作内容一小部分。在微服务开发模式下,通过SkyWalking可以得知服务健康度,接口性能,在哪个环节耗时最久。
但是对于通过@PathVariable
方式定义的Controller层接口,则无能为力,不同的channel
和userId
会组合出无数种接口统计数据:
参考
- Feign反序列化MismatchedInputException:Cannot deserialize instance of
Boolean
out of START_OBJECT token - No message available问题解决