Spring Cloud Open Feign实战

Feign是一个简化HTTP请求的框架,通过注解将HTTP请求模板化,支持服务发现和负载均衡。在微服务环境中,Feign可以作为RPC方案,结合Ribbon和Hystrix提供服务间的解耦和容错管理。文章介绍了Feign的工作原理,包括动态代理的构建和元数据解析,并展示了通过Feign上传MultipartFile的示例,以及在实际使用中遇到的问题和解决方案,如Feign反序列化问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概述

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特性:

  1. 可插拔的注解支持,包括Feign和JAX-RS注解
  2. 支持可插拔的HTTP编码器和解码器
  3. 支持Hystrix和它的Fallback
  4. 支持Ribbon的负载均衡
  5. 支持HTTP请求和响应的压缩

Open Feign提供的常用注解
在这里插入图片描述

@FeignClient

@FeignClient注解源码如下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface FeignClient {
    @AliasFor("name")
    String value() default "";
    String contextId() default "";
    @AliasFor("value")
    String name() default "";
    String[] qualifiers() default {};
    String url() default "";
    boolean dismiss404() default false;
    Class<?>[] configuration() default {};
	// 指定Hystrix fallback
    Class<?> fallback() default void.class;
    Class<?> fallbackFactory() default void.class;
    String path() default "";
    boolean primary() default true;
}

方法解释:

  • value:指定的唯一服务名,也就是注册到Nacos、Consul、Eureka等注册中心的服务名,作用同name方法
  • 如果同时指定value和name,且两者配置不一样,会怎样?
  • contextId:
  • qualifiers:
  • url:直接填写硬编码的URL地址
  • dismiss404:404是被解码,还是抛异常
  • configuration:返回值是数组,意味着可以配置多个类,默认为空。对应的配置类为FeignClientsConfiguration类,默认注入Decoder、Encoder和Contract等配置的Bean
  • 关于Hystrix Fallback,参考FallbackFactory使用
  • path
  • primary

配置

对应于FeignClientsConfiguration,默认注入很多Feign相关的配置Bean,包括FeignRetryer、FeignLoggerFactory和FormattingConversionService等。Decoder、Encoder和Contract这3个类在没有Bean被注入的情况下,会自动注入默认配置的Bean,即ResponseEntity Decoder、SpringEncoder和SpringMvcContract。

关于自定义配置类,参考下面的实战环节。

日志

对应的源码:

public abstract class Logger {
	// 省略其他代码
    public static enum Level {
        NONE,
        BASIC,
        HEADERS,
        FULL;
    }
}

日志是配置的一部分,Logger.Level有4个日志级别:

  • NONE:不记录任何信息,默认
  • BASIC:仅记录请求方法、URL以及响应状态码和执行时间
  • HEADERS:除了记录 BASIC级别的信息外,还会记录请求和响应的头信息
  • FULL:记录所有请求与响应的明细,包括头信息、请求体、元数据

修改默认的日志级别:

@Configuration
public class FeignConfig {
    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

然后在@FeignClient的configuration方法里引用FeignConfig.class。

原理

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,专门用于解析SpringMVC标签。

实战

分布式、微服务开发中,文件服务是一个必备且常见的组件,用于提供文件上传、下载、获取文件地址、获取图片预览URL等功能。假设此服务名为FS,即File-Service的缩写,可对前端提供服务,此时提供Controller层接口即可;也可对后端服务提供接口,此时则最好暴露出一个fs-client依赖出来。

另外,FS服务隐藏具体的实现细节,如对接第三方文件存储服务,如阿里云,七牛云等;FS负责维护具体的endpoint、accessKeyId、accessKeySecret,服务调用方(包括前端和后端服务),不关心这些细节。

通过Feign上传MultipartFile

现有FS服务,有一个Controller层接口:

@PostMapping(value = "/ossUploadPrivateFile")
public Response<UploadFileVO> ossUploadPrivateFile(@RequestPart(value = "file") MultipartFile multipartFile) {
}

微服务开发模式下,其他后端服务想要使用FS服务提供的接口,则FS服务需提供一个即jar包,即新增一个fs-client maven 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层接口,则无能为力,不同的channeluserId会组合出无数种接口统计数据:
    在这里插入图片描述

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

johnny233

晚饭能不能加鸡腿就靠你了

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值