OpenFeign 开发随笔

OpenFeign 底层实现原理

失败重试机制的底层实现原理

  • (1) 添加注解
    • 在主启动类或者配置类上添加 @EnableFeignClients 注解。
  • (2) 动态代理
    • @EnableFeignClients 注解会触发 SpringBoot 框架的自动配置机制,扫描所有标记有 @FeignClient 的接口,并为它们创建代理实例。
  • (3) RequestTemplate 发送 HTTP 请求
    • 此处的 RequeustTemplate 可以理解为 RestTemplate,因为两者的目的相同。
    • OpenFeign 不能直接发送 HTTP 请求,它在动态代理里面做了一些事情,也就是将注解里面请求的路由地址取出来,然后拼接出来一个 URL 请求的地址,然后再使用 RequestTemplate(RestTemplate)去发送 HTTP 请求。
  • (4) RestTemplate 依靠 HTTP 框架实现 Web 请求的发送
    • RestTemplate 只是一个模板方法类,它只是规定了一些调用的 API,底层并没有真正的实现,它依靠 HTTP 框架实现 Web 请求的发送(比如 Apache Http Client、Okhttp)。

OpenFeign 整合第三方框架

整合 Sentinel 失败

错误描述

使用以下组件版本将 OpenFeign 整合 Sentinel:

组件版本说明
Spring Boot3.2.0
Spring Cloud2023.0.0
Spring Cloud Alibaba2022.0.0.0

在微服务应用启动时,会抛出了以下异常信息:

1
2
3
4
Caused by: java.lang.IllegalStateException: Method ProviderFeignApi#getPayByOrderNumber(String) not annotated with HTTP method type (ex. GET, POST)
Warnings:
- Class ProviderFeignApi has annotations [FeignClient] that are not used by contract Default
- Method getPayByOrderNumber has an annotation GetMapping that is not used by contract Default

错误分析

微服务应用启动失败的原因是 Spring Boot 和 SpringCloud 的版本过高,导致 Spring Cloud Alibaba Sentinel 与 OpenFeign 不兼容。

第一种解决办法

耐心等待 Spring Cloud Alibaba 官方适配高版本的 Spring Boot 和 SpringCloud,从而解决版本兼容问题。

第二种解决办法

降低 Spring Boot 和 SpringCloud 的版本,比如使用以下组件版本:

组件版本说明
Spring Boot3.0.9
Spring Cloud2022.0.2
Spring Cloud Alibaba2022.0.0.0

第三种解决方案

重写 com.alibaba.cloud.sentinel.feign.SentinelFeignAutoConfiguration 自动配置类,最关键的是重写 feign.Feign.Builder 类的 internalBuild() 方法,并在 SentinelFeignAutoConfiguration 类加载前将重写的自动配置类注入到 Spring IOC 容器中。值得一提的是,修复版本兼容问题的完整案例代码可以从 这里 下载得到,并且 OpenFeign 整合 Sentinel 的详细教程还可以看 这里

参考项目

OpenFeign 常见的使用错误

Request method ‘POST’ not supported

  • 错误信息:在调用 OpenFeign 下面这段代码时,抛出了 Request method 'POST' not supported 的异常
1
2
3
4
5
6
7
8
9
10
/**
* 服务调用方
*/
@FeignClient(name = MicroServiceName.WECHAT_SERVICE)
public interface WechatSubscribeUserService {

@RequestMapping(value = "/wechat/subscribe/get", method = RequestMethod.GET)
WechatSubscribeUser getSubscribeUser(@RequestBody WechatSubscribeUserVo vo);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 服务提供方
*/
@RestController
@RequestMapping("/wechat/subscribe")
public class WechatSubscribeUserController {

@Autowired
private WechatSubscribeUserService subscribeUserService;

@GetMapping("/get")
public WechatSubscribeUser get(WechatSubscribeUserVo vo) {
String toOpenId = vo.getToOpenId();
String fromOpenId = vo.getFromOpenId();
return subscribeUserService.getUser(fromOpenId, toOpenId);
}

}
  • 错误原因:OpenFeign 原生的连接工具默认使用了 JDK 中的 HttpURLConnection 类进行实现,下面这段代码是在 HttpURLConnection 中发现的,所以只要 HTTP 请求里有 Body 体对象,就会强制的把 GET 请求转换成 POST 请求。
1
2
3
4
5
6
7
8
9
10
11
private synchronized OutputStream getOutputStream0() throws IOException {
try {
if (!this.doOutput) {
throw new ProtocolException("cannot write to a URLConnection if doOutput=false - call setDoOutput(true)");
} else {
if (this.method.equals("GET")) {
this.method = "POST";
}
}
}
}
  • 第一种解决方案:不使用 POJO 对象作为参数,而是传入多个独立的参数,并添加 @RequestParam 注解
1
2
3
4
5
6
7
8
9
10
/**
* 服务调用方
*/
@FeignClient(name = MicroServiceName.WECHAT_SERVICE)
public interface WechatSubscribeUserService {

@RequestMapping(value = "/wechat/subscribe/get", method = RequestMethod.GET)
WechatSubscribeUser getSubscribeUser(@RequestParam("fromOpenId") String fromOpenId, @RequestParam("toOpenId") String toOpenId);

}
  • 第二种解决方案:使用 POJO 对象作为参数,同时添加 @SpringQueryMap 注解
1
2
3
4
5
6
7
8
9
10
/**
* 服务调用方
*/
@FeignClient(name = MicroServiceName.WECHAT_SERVICE)
public interface WechatSubscribeUserService {

@RequestMapping(value = "/wechat/subscribe/get", method = RequestMethod.GET)
WechatSubscribeUser get(@SpringQueryMap WechatSubscribeUserVo vo);

}

提示

Feign 的 @QueryMap 注解支持将 POJO 用作 GET 请求的参数映射,但默认的 @QueryMap 注解与 Spring 不兼容,因为它缺少 value 属性。Spring Cloud OpenFeign 提供了等效的 @SpringQueryMap 注解,用于将 POJO 或 Map 参数映射为查询参数。简而言之,Feign 的 GET 请求无法解析对象参数,如果传参是一个类对象,框架就需要把这个类对象解析成查询参数,但是直接在方法中传参框架不会自动把类对象解析成查询参数。@SpringQueryMap 注解的作用就是把 POJO 解析成 k1=v1&k2=v2 的查询参数格式。

  • 第三种解决方案:使用 Apache HttpClient 或者 OkHttp 替换掉 OpenFeign 原生使用的 HttpURLConnection 连接工具,然后添加 @RequestBody 注解
1
2
3
4
5
6
7
8
9
10
/**
* 服务调用方
*/
@FeignClient(name = MicroServiceName.WECHAT_SERVICE)
public interface WechatSubscribeUserService {

@RequestMapping(value = "/wechat/subscribe/get", method = RequestMethod.GET)
WechatSubscribeUser getSubscribeUser(@RequestBody WechatSubscribeUserVo vo);

}

warning

笔者尝试使用 Apache HttpClient 或者 OkHttp 替换 OpenFeign 默认使用的 HttpURLConnection,但并没有生效,有兴趣的可以参考这里的 替换教程