SpringCloud(1-5)OpenFeign底层原理拦截机制OpenFeign:声明式 RESTful 客户端
类似于 RestTemplate ,OpenFeign 是对 JDK 的 HttpURLConnection(以及第三⽅库 HttpClient 和 OkHttp)的包装和简化,并且还⾃动整合了Ribbon 。
1. 什么是 OpenFeign
Feign 早先由 Netflix 公司提供并开源,在它的8.18.0之后,Nefflix 将其捐赠给 Spring Cloud 社区,并更名为 OpenFeign 。OpenFeign 的第⼀个版本就是9.0.0。
OpenFeign 会完全代理 HTTP 的请求,在使⽤过程中我们只需要依赖注⼊ Bean,然后调⽤对应的⽅法传递参数即可。这对程序员⽽⾔屏蔽了 HTTP 的请求响应过程,让代码更趋近于『调⽤』的形式。
2. Feign 的⼊门案例
2.1 启动 Nacos 注册中⼼
启动你本地(或服务器)上的 Nacos Server ,确保其正在运⾏。
2.2 创建被调⽤服务
注意
在调⽤和被调关系中,被调⽅是不需要 OpenFeign 的,主调⽅才需要。
创建⼀个 Spring Boot Maven 项⽬作为被调⽅,命名为 b-service(或其他),确保:
1. 对外暴露出⼀个 URL ,即,对外提供⼀个功能。未来,我们的 a-service 会向这个 URL 发出 HTTP 请求,触发 b-service 的这个功能的执⾏,并从 b-
service 这⾥获得 HTTP 响应。
2. b-service 能启动、运⾏,并能连上 Nacos Server ,即,在 Nacos Server 上能看到 b-service 。
2.3 创建主调服务
创建⼀个 Spring Boot Maven 项⽬作为主调⽅(调⽤发起⽅、HTTP 请求发起⽅),命名为 a-service(或其他)。
1. 在 Spring Initializer 中引⼊依赖:在 Initializer 的搜索框内搜索并选择 Spring Web 、 Nacos Service Discovery 和 OpenFeign 。
注意
这⾥⾃动引⼊的 OpenFeign 的 maven 依赖为:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
Copied!
2. 为项⽬添加配置 application.yaml :
server:
port: 8080
spring:
application:
name: a-service
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
namespace: public
# group: hemiao
Copied!
3. 最后创建⼀个启动类 AserviceApplication:
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "...") // 看这⾥,看这⾥,看这⾥
public class AserviceApplication {
public static void main(String[] args) {
SpringApplication.run(AserviceApplication.class, args);
}
}
Copied!
我们可以看到启动类增加了⼀个新的注解: @EnableFeignClients,如果我们要使⽤ OpenFeign ,必须要在启动类加⼊这个注解,以开启OpenFeign 。
这样,我们的 Feign 就已经集成完成了,那么如何通过 Feign 去调⽤之前我们写的 HTTP 接⼝呢?
和 MyBatis 类似:
⾸先创建⼀个接⼝ BServiceClient(名字任意),并且通过注解配置要调⽤的服务地址:
@FeignClient(value = "b-service") // 这⾥要和 b-service 在 nacos-server 上登记的名字相呼应
public interface BServiceClient {
@GetMapping("/")
public String index();
@PostMapping("/login1")
public String login1(@RequestParam("username") String username,
@RequestParam("uesrname") String password);
@PostMapping("/login2")
public String login2(@SpringQueryMap LoginToken token);
@PostMapping("/login3")
public String login3(@RequestBody LoginToken token);
}
Copied!
@FeignClient 注解的 name 属性的值是被调⽅(也就是服务的提供者)在 Nacos 注册中⼼上所注册的名字,通常也就是被调⽅(服务提供
者)的 spring.application.name 。
注意
⼀个服务只能被⼀个类绑定,不能让多个类绑定同⼀个远程服务,否则,会在启动项⽬是出现 “已绑定” 异常。
然后在 OpenFeign ⾥⾯通过单元测试来查看效果。
@Test
public void test() {
try {
log.debug("{}", bService.index());
} catch (Exception e) {
e.printStackTrace();
}
}
Copied!
说明
OpenFeign 的能⼒包括但不仅包括这个。
3. FeignClient 抛出异常
当调⽤⽅ b-service 正常返回时,b-service(的 Spring MVC)的返回就是正常的 HTTP 200 响应,⽽在 a-service 这边,Openfeign 会帮我们做数据(从 HTTP 响应体中的)提取、转换操作,并从 FeignClient 中返回。
当被调⽅ b-service 返回的是⾮ 200 的响应(⽐如,500、429 等)时,在 a-service 这边,Openfeign 则会在 FeignClient ⽅法中抛出⼀个异
常(⼀个 RuntimeException 的⼦类)。
4. OpenFeign 的配置
4.1 超时和超时重试
OpenFeign 本⾝也具备重试能⼒,在早期的 Spring Cloud 中,OpenFeign 默认使⽤的是 feign.Retryer.Default#Default ,重试 5 次。但
OpenFeign 整合了 Ribbon ,⽽ Ribbon 也有重试的能⼒,此时,就可能会导致⾏为的混乱。(总重试次数 = OpenFeign 重试次数 x Ribbon 的重试次数,这是⼀个笛卡尔积。)
后来 Spring Cloud 意识到了此问题,因此做了改进(),将 OpenFeign 的默认重试改为 feign.Retryer#NEVER_RETRY ,即,默认关闭。
简单来说,OpenFeign 对外表现出的超时和重试的⾏为,实际上是它所⽤到的 Ribbon 的超时和超时重试⾏为。我们在项⽬中进⾏的配置,也都是配置 Ribbon 的超时和超时重试。
# 全局配置
ribbon:
readTimeout: 1000 # 请求处理的超时时间
MaxAutoRetries: 5 # 最⼤重试次数
MaxAutoRetriesNextServer: 1 # 切换实例的重试次数
# 是否开启对所有请求进⾏超时重试。⼀般不会开启这个功能。默认值是 false ,表⽰仅对 get 请求进⾏超时重试
# okToRetryOnAllOperations: true
Copied!
整个 OpenFeign(实际上是 Ribbon)的最⼤重试次数为:
(1 + MaxAutoRetries) x (1 + MaxAutoRetriesNextServer)
Copied!
这⾥需要注意的是『重试』次数是不包含『本⾝那⼀次』的。
故意加⼤被调服务的返回响应时长,你会看到主调服务中打印类似如下消息:
feign.RetryableException: Read timed out executing GET SERVICE-PRODUCER/demo?username=tom&password=123
at Executing(FeignException.java:249)
at uteAndDecode(SynchronousMethodHandler.java:129)
at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:89)
...
Copied!
另外,在被调服务⽅,你会发现上述配置会导致被调服务收到 12 次请求:
请求次数 = (1 + 5) x (1 + 1)
Copied!
你也可以指定对某个特定服务的超时和超时重试:
# 针对⾃⼰向 b-service 发出请求超时的设置
b-service:
ribbon:
readTimeout: 3000
MaxAutoRetries: 2
MaxAutoRetriesNextServer: 0
Copied!
4.2 替换底层 HTTP 实现(了解)
类似 RestTemplate,本质上是 OpenFeign 的底层会⽤到 JDK 的HttpURLConnection 发出 HTTP 请求。另外,如果有需要,你也可以换成第三⽅库 HttpClient 或 OkHttp 。
替换成 HTTPClient
将 OpenFeign 的底层 HTTP 客户端替换成 HTTPClient 需要 2 步:
1. 引⼊依赖:
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
Copied!
2. 在配置⽂件中启⽤它:
feign:
httpclient:
enabled: true # 激活 httpclient 的使⽤
替换成 OkHttp
将 OpenFeign 的底层 HTTP 客户端替换成 OkHttp 需要 2 步:
1. 引⼊依赖:
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
Copied!
2. 在配置⽂件中启⽤它:
feign:
okhttp:
enabled: true # 激活 okhttp 的使⽤
4.3 ⽇志配置(了解)
SpringCloudFeign 为每⼀个 FeignClient 都提供了⼀个 feign.Logger 实例。可以根据 logging.level.<FeignClient> 参数配置格式来开启Feign 客户端的 DEBUG ⽇志,其中 <FeignClient> 部分为 Feign 客户端定义接⼝的完整路径。如:
logging:
level:
com.woniu.outlet.client: DEBUG
Copied!
然后再在配置类(⽐如主程序⼊⼝类)中加⼊ Looger.Level 的 Bean:
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
Copied!
级别说明
NONE不输出任何⽇志
BASIC只输出 Http ⽅法名称、请求 URL、返回状态码和执⾏时间
HEADERS输出 Http ⽅法名称、请求 URL、返回状态码和执⾏时间和 Header 信息
FULL记录 Request 和 Response 的 Header,Body 和⼀些请求元数据
5. OpenFeign 的底层原理概述
虽然在使⽤ OpenFeign 时,我们(程序员)定义的是接⼝,但是 OpenFeign 框架会通过 JDK 动态代理⽣成 @FeignClient 接⼝的代理对象。逻辑相当于:
@Autowired
XxxServiceClient client = wProxyInstance(invocationHandler);
Copied!
在这⾥,出现了⼀个 InvocationHandler 对象,结合 JDK 动态代理的知识,我们知道,当你调⽤ client 的某个⽅法时,实际上触发的就是这个 InvocationHandler 对象的 invoke ⽅法。InvocationHandler 对象逻辑相当于:
public class SimpleInvocationHandler implements InvocationHandler {
Map<Method, MethodHandler> methodToHandler = new LinkedHashMap();
public SimpleInvocationHandler(Map<Method, MethodHandler> methodToHandler) {
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
MethodHandler handler = (method);
return handler.invoke();
}
}
Copied!
在 InvocationHandler 中最核⼼的在于它有⼀个 Map ,这个 map 以 InvocationHandler 所代理的那个 FeignClient 中所声明的⽅法的Method 对象为 key ,值是⼀个⼀个的 MethodHandler 对象。
假设有⼀个 @FeignClient 为如下形式:
@FeignClient("a-service")
public interface AService {
@RequestMapping("/hello")
public String hello();
@RequestMapping("/world")
public String world();
}
Copied!
那么,AService 有⼀个代理对象 InvocationHandler ,它⾥⾯的 Map 逻辑上形如:
key value
helloMethod helloMethodHandler
worldMethod worldMethodHandler
那么,当你调⽤bService.hello();⽅法时,实际上是 InvocationHandler 对象的 invoke ⽅法被执⾏,⽽ InvocationHandler 对象会从它的 Map 中以 hello ⽅法的 Method 对象为 key 到对应的⼀个 MethodHandler 对象,然后调⽤ MethodHandler 对象的 invoke ⽅法。
调⽤关系和流程形如:
bService.hello()
└──> invocationHandler.invoke()
└──> methodHandler.invoke()
├──> 第⼀件事 ...
└──> 第⼆件事 ...
Copied!
MethodHandler 的 invoke() ⽅法核⼼就是⼲了 2 件事情:
1. 传给 Ribbon ⽬标服务的服务名,它 “要” ⼀个该服务的实例的具体的地址;
2. 根据 Ribbon 返回的具体地址,发出 HTTP 请求,并等待、解析响应。
6. OpenFeign 的机制
OpenFeign 有⼀个机制,对于它的作⽤ OpenFeign 的官⽅是这样描述的:
Zero or more may be configured for purposes such as adding headers to all requests.
Copied!
你可以⾃定义类继承 RequestInterceptor ,当然,你也可以使⽤ lambda 表达式结合 @Bean 进⾏简化:
@Bean
public RequestInterceptor requestInterceptor() {
return requestTemplate -> {
requestTemplate.header("x-jwt-token", "...");
};
}
Copied!
下⾯代码是将当前请求的所有请求头添加到 openfeign 将要发出的请求中:
@Beanspringcloud和springboot
public RequestInterceptor requestInterceptor() {
return requestTemplate -> {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestAttributes();
HttpServletRequest request = Request();
Enumeration<String> headerNames = HeaderNames();
if (headerNames == null)
return;
while (headerNames.hasMoreElements()) {
String name = Element();
String values = Header(name);
requestTemplate.header(name, values);
}
}
}
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论