1.认识 Feign
在之前调用生产者的服务是使用 RestTemplate,这种是 HTTP 请求调用,而对于服务调用的终极方案是使用
OpenFeign。
1.1、OpenFeign 简介
OpenFeign = Fegin
Feign is a declarative web service client. It makes writing web service clients easier. To use Feign create an interface and annotate it. It has pluggable annotation support including Feign annotations and JAX-RS annotations. Feign also supports pluggable encoders and decoders. Spring Cloud adds support for Spring MVC annotations and for using the same HttpMessageConverters used by default in Spring Web. Spring Cloud integrates Eureka, Spring Cloud CircuitBreaker, as well as Spring Cloud LoadBalancer to provide a load-balanced http client when using Feign.
是一个 web 客户端,想要使用 Feign 只需要使用一个接口以及注解,feign 能够支持插件式注解,并且提供编解码器,在 springcloud 中也有对 springmvc 的注解支持,并且对于Feign 本身就自带了负载均衡器。
Feign 是一个远程调用的组件 (接口,注解) ,进行服务和服务之间的 http 调用的,是一款 RPC 框架
Feign 集成了 ribbon ,ribbon 里面集成了 eureka。
feign 底层默认是 ribbon,采用的是轮询算法。
2.Feign 实战
2.1、案例 1:使用 feign 来进行远程调用服务
背景:
版本:SpringBoot:2.3.12.RELEASE;Spring-Cloud:Hoxton.SR12。
说明:创建两个服务:order 以及 user。在 user 服务中会去使用 feign 来进行调用 order 服务。
因为 user-service 里面没有下单的功能,需要 user-service 去调用 order-service 才可以下单
但是前端直接访问的服务是 user-service
==分布式调用思想:我不会,别人会,就去调用别人!==——>微服务之间的相互调用!
1、创建 Order 服务
创建 SpringBoot 项目,选择依赖如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
① 配置文件:applicaion.yaml
server:
port: 8081
spring:
application:
name: order-service
eureka:
client:
service-url:
defaultZone: localhost:8761/eureka
② 在启动器上添加开启 eurekaclient 注解
@EnableEurekaClient //开启服务注册
③ 创建一个服务,用于对外提供服务
package com.changlu.orderservice.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
/**
* @Description:
* @Author: changlu
* @Date: 6:45 PM
*/
@RestController
public class OrderController {
@GetMapping("/doOrder")
public String doOrder() {
return "牛奶泡芙";
}
}
2、创建 User 服务(使用 feign 来进行远程调用 order 服务)
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--openfeign的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
① 配置文件:applicaion.yaml
server:
port: 8081
spring:
application:
name: order-service
eureka:
client:
service-url:
defaultZone: localhost:8761/eureka
② 在启动器上添加开启 eurekaclient 注解以及开启 openfeign 注解(扫描包)
@EnableEurekaClient //开启服务注册
@EnableFeignClients //开启feign客户端扫描!!!
③ 创建对应的 feign 接口(对应 order 服务的 controller 方法)
UserOrderFeign.java:
package com.changlu.userservice.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
/**
* @Description: 远程调用器
* @Author: changlu
* @Date: 6:52 PM
*/
@FeignClient(value = "order-service")//value表示服务名
public interface UserOrderFeign {
/**
* 你需要调用哪个controller 就写它的方法签名
* 方法签名(就是包含一个方法的所有的属性)
原方法:
@GetMapping("/doOrder")
public String doOrder() {
return "牛奶泡芙";
}
现在写成接口样式的:
*/
@GetMapping("/doOrder")
public String doOrder();
}
④ 创建 controller,进行依赖注入发起远程调用
UserController.java:
package com.changlu.userservice.controller;
import com.changlu.userservice.feign.UserOrderFeign;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Description:
* @Author: changlu
* @Date: 6:50 PM
*/
@RestController
public class UserController {
@Autowired
private UserOrderFeign userOrderFeign; //引入远程调用器类
@GetMapping("/userDoOrder")
public String userDoOrder() {
System.out.println("用户来访问接口:/userDoOrder");
//发起远程调用
String res = userOrderFeign.doOrder();
return res;
}
}
测试案例
我们启动之前的 eureka 服务,接着启动自己的编写的两个服务:
此时我们来访问:localhost:8082/userDoOrder
可以看到远程调用成功!
2.2、超时配置处理
1、超时异常复现
在当前的版本背景当中,远程调用 openfeign 的默认超时时长为 1s。
我们修改一下 Order 服务中的处理时长为 2s,来看看是否会出现超时异常:
@GetMapping("/doOrder")
public String doOrder() {
//这里睡眠两秒
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "牛奶泡芙";
}
接着我们重启下 order 服务,然后去访问网址:localhost:8082/userDoOrder
看下报错信息:
2、方案:配置超时时长,解决报错异常
在 User 服务中修改配置文件:
ribbon: #feign 默认调用 1s 超时
ReadTimeout: 3000 #修改调用时长为 5s
ConnectTimeOut: 5000 # 修改连接时长为5s
此时再访问地址:localhost:8082/userDoOrder
可以看到能够得到访问结果。
2.3、案例 2:参数传递案例(单独日期特别处理)
1、常见请求体+参数案例
我们在 Order 服务中添加请求参数的一些请求接口:
ParamController.java:
package com.changlu.orderservice.controller;
import com.changlu.orderservice.domain.Order;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
/**
* url /doOrder/热干面/add/油条/aaa
* get传递一个参数
* get传递多个参数
* post传递一个对象
* post传递一个对象+一个基本参数
*/
@RestController
public class ParamController {
//url参数:PathVariable
@GetMapping("testUrl/{name}/and/{age}")
public String testUrl(@PathVariable("name") String name, @PathVariable("age") Integer age) {
System.out.println(name + ":" + age);
return "ok";
}
//请求参数:?xx=xx
@GetMapping("oneParam")
public String oneParam(@RequestParam(required = false) String name) {
System.out.println(name);
return "ok";
}
//请求参数:?xx=xx&xx=xx
@GetMapping("twoParam")
public String twoParam(@RequestParam(required = false) String name, @RequestParam(required = false) Integer age) {
System.out.println(name);
System.out.println(age);
return "ok";
}
//请求体参数
@PostMapping("oneObj")
public String oneObj(@RequestBody Order order) {
System.out.println(order);
return "ok";
}
//请求体 + 请求参数url中的xx=xx
@PostMapping("oneObjOneParam")
public String oneObjOneParam(@RequestBody Order order, @RequestParam("name") String name) {
System.out.println(name);
System.out.println(order);
return "ok";
}
}
Order.java
@Builder 注解
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder //添加这个注解可以方便快速构建实体类的对象!!!
public class Order {
private Integer id;
private String name;
private Double price;
private Date time;
}
接下来写这两个文件:
接着我们来在 user 服务中添加对应的 feign 的接口方法,然后再 UserController 的某个接口方法里来进行远程调用:
UserOrderFeign.java:
package com.changlu.userservice.feign;
import com.changlu.userservice.domain.Order;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
/**
* @Description: 远程调用器
* @Author: changlu
* @Date: 6:52 PM
*/
@FeignClient(value = "order-service")//value表示服务名
public interface UserOrderFeign {
/**
* 你需要调用哪个controller 就写它的方法签名
* 方法签名(就是包含一个方法的所有的属性)
*/
@GetMapping("/doOrder")
public String doOrder();
//*****请求参数测试案例*****
//url参数:PathVariable
@GetMapping("testUrl/{name}/and/{age}")
public String testUrl(@PathVariable("name") String name, @PathVariable("age") Integer age);
//请求参数:?xx=xx
@GetMapping("oneParam")
public String oneParam(@RequestParam(required = false) String name);
//请求参数:?xx=xx&xx=xx
@GetMapping("twoParam")
public String twoParam(@RequestParam(required = false) String name, @RequestParam(required = false) Integer age);
//请求体参数
@PostMapping("oneObj")
public String oneObj(@RequestBody Order order);
//请求体 + 请求参数url中的xx=xx
@PostMapping("oneObjOneParam")
public String oneObjOneParam(@RequestBody Order order,@RequestParam("name") String name);
}
UserController.java:
package com.changlu.userservice.controller;
import com.changlu.userservice.domain.Order;
import com.changlu.userservice.feign.UserOrderFeign;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
/**
* @Description:
* @Author: changlu
* @Date: 6:50 PM
*/
@RestController
public class UserController {
@GetMapping("/testParam")
public String testParam() {
//第一个:url路径携带参数
String s1 = userOrderFeign.testUrl("changlu", 18);
System.out.println(s1);
//第二个:1个请求参数
String s2 = userOrderFeign.oneParam("changlu");
System.out.println(s2);
//第三个:两个请求参数
String s3 = userOrderFeign.twoParam("changlu", 666);
System.out.println(s3);
Order order = Order.builder()
.name("泡芙")
.price(1000.0)
.time(new Date())
.id(1)
.build();
//第四个:请求体
String s4 = userOrderFeign.oneObj(order);
System.out.println(s4);
//第五个:第一个请求体 + 参数
String s5 = userOrderFeign.oneObjOneParam(order, "changlu");
System.out.println(s5);
return "ok";
}
}
测试一下:
2、单独日期传递【特别注意】
Order 服务:
//ParamController
//时间请求参数
@GetMapping("testTime")
public String testTime(@RequestParam Date date){
System.out.println(date);
return "ok";
}
User 服务:
//接口:UserOrderFeign
@GetMapping("testTime")
public String testTime(@RequestParam Date date);
//控制器类:UserController
/**
* Sun Mar 20 10:24:13 CST 2022
* Mon Mar 21 00:24:13 CST 2022 +- 14个小时
* 1.不建议单独传递时间参数
* 2.转成字符串(推荐,比较好的方案,在服务端使用String接收,然后可以进行format转为date对象)
2022-03-20 10:25:55:213 因为字符串不会改变
* 3.使用jdk LocalDate 年月日 没问题 ,LocalDateTime 会丢失秒
* 4.改feign的源码
*
* @return
*/
@GetMapping("/testTime")
public String testTime(){
//错误示例1:使用new Date();
userOrderFeign.testTime(new Date());
//正确方案:服务端的请求接口不应该使用Date来进行接收,尽可能使用字符串、LocalDate来进行传递
return "ok";
}
错误案例 Order 服务打印结果(时间不准确):Sun Jul 17 10:26:20 CST 2022
2.4、日志处理
需求:若是我们想要知道每一个请求的详细请求参数,那么我们可以来进行打开日志配置以及来注入相应的日志等级。
操作 1:在启动器类或者配置类中注入一个日志等级。
/**
* 打印fein日志信息 级别
* @return
*/
@Bean
public Logger.Level level(){
return Logger.Level.FULL;
}
操作 2:在配置文件 yaml 中打开配置。
# 日志等级选择
logging:
level:
com.changlu.userservice.feign.UserOrderFeign: debug # 我需要输出这个接口下面的方法的日志
接着我们来测试一下:访问网址 localhost:8082/testTime
此时就可以看到 feign 发起请求的一系列参数如下:
3.手写 feign 简易实现
本质:feign 本质就是进行发送 http 请求,并且再此基础上具备负载均衡的功能,我们来对其进行复现一下。
思考:为什么 UserOrderFeign 接口可以直接被调用方法呢?——> 动态代理
* 接口是不能做事情的
* 如果想做事 必须要有对象
* 那么这个接口肯定是被创建出代理对象的
* 动态代理 jdk(java interface 接口 $Proxy ) 或者 cglib(subClass 子类)
* jdk动态代理 只要是代理对象调用的方法必须走 java.lang.reflect.InvocationHandler.invoke(java.lang.Object, java.lang.reflect.Method, java.lang.Object[])
我们来接着 Feign 实战中写的 User 服务中 UserOrderFeign 接口,来对该接口中的 doOrder 方法进行代理!(相当于我们不用默认的代理,我们来自己重写,会走我们写的方法)
① 首先我们来增强 RestTemplate 方法,另起能够具备负载均衡的效果。
在对应的启动器类 UserserviceApplication 中添加该 bean:
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
② 之后我们在 test 测试包中来进行代理类编写
package com.changlu.userservice;
import com.changlu.userservice.controller.UserController;
import com.changlu.userservice.feign.UserOrderFeign;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.client.RestTemplate;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
@SpringBootTest
class UserserviceApplicationTests {
@Autowired
private RestTemplate restTemplate;
@Test
void contextLoads() {
//给接口创建一个代理对象
UserOrderFeign feign = (UserOrderFeign)Proxy.newProxyInstance(UserController.class.getClassLoader(), new Class[]{
UserOrderFeign.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//这主要就是feign的核心源码:
String res = null;
if (method.getName().equals("doOrder")) {
//1、获取到对应的path路径
// 能去拿到对方的ip和port 并且拿到这个接口方法上面的注解里面的值 那么就完事了
GetMapping annotation = method.getAnnotation(GetMapping.class);//拿到UserOrderFeign类里面的doOrder方法上面的注解
String[] paths = annotation.value(); //拿到注解的所有参数值
String path = paths[0];
//2、根据对应的的方法来获取到相应的class字节码
Class<?> aclass = method.getDeclaringClass();
//3、获取到对应feign类的注解,取得对应的服务名
FeignClient fannoation = aclass.getAnnotation(FeignClient.class);//拿到UserOrderFeign类上面的注解
String serviceName = fannoation.value();
//4、拼接服务地址
String url = "http://" + serviceName + path;
//5、发送请求(使用resttemplate来进行发送请求)
res = restTemplate.getForObject(url, String.class);
}
return res;
}
}
);
System.out.println(feign.doOrder());
}
}
测试成功!
4.feign 的源码分析
4.1、OpenFeign 的原理分析
使用动态代理 jdk (invoke) 及 cglib 子类继承的 :
源码简述:
1、给接口创建代理对象(启动扫描)
2、代理对象执行进入 invoke 方法
3、在 invoke 方法里面做远程调用
具体流程:
1、通过注解扫描来获取到调用服务名称以及对应的接口 url。
3、最终发起请求,来进行远程调用。
4.2、如何扫描注解@FeignClient?
在启动器上我们添加了一个开启 Feign 扫描的注解:
@EnableFeignClients //开启feign客户端扫描
//看下对应的注解源码
@Retention(RetentionPolicy.RUNTIME)
@Target({
ElementType.TYPE})
@Documented
@Import({
FeignClientsRegistrar.class}) //FeignClientsRegistrar是Feign的注册类
public @interface EnableFeignClients {
String[] value() default {
};
String[] basePackages() default {
};
Class<?>[] basePackageClasses() default {
};
Class<?>[] defaultConfiguration() default {
};
Class<?>[] clients() default {
};
}
进入 FeignClientsRegistrar 这个类 去查看里面的东西
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
this.registerDefaultConfiguration(metadata, registry);
//扫描注解来进行一个注册(@FeignClient)
this.registerFeignClients(metadata, registry);
}
//AnnotationMetadata即注解的元信息
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet();
//获取到启动类上的feign注解
Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
Class<?>[] clients = attrs == null ? null : (Class[])((Class[])attrs.get("clients"));
if (clients != null && clients.length != 0) {
Class[] var12 = clients;
int var14 = clients.length;
for(int var16 = 0; var16 < var14; ++var16) {
Class<?> clazz = var12[var16];
candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
}
} else {
ClassPathScanningCandidateComponentProvider scanner = this.getScanner();
scanner.setResourceLoader(this.resourceLoader);
scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
Set<String> basePackages = this.getBasePackages(metadata);
Iterator var8 = basePackages.iterator();
//循环遍历得到对应的包名
while(var8.hasNext()) {
String basePackage = (String)var8.next();
candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
}
}
Iterator var13 = candidateComponents.iterator();
while(var13.hasNext()) {
BeanDefinition candidateComponent = (BeanDefinition)var13.next();
if (candidateComponent instanceof AnnotatedBeanDefinition) {
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition)candidateComponent;
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface");
//拿到接口上的注解
Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(FeignClient.class.getCanonicalName());
String name = this.getClientName(attributes);
this.registerClientConfiguration(registry, name, attributes.get("configuration"));
//将创建的代理对象registry交给spring
this.registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
4.3、如何创建代理对象去执行调用?
在启动时,在 ReflectiveFeign 的 newInstance 方法中,给接口创建了代理对象
public class ReflectiveFeign extends Feign {
public <T> T newInstance(Target<T> target) {
Map<String, MethodHandler> nameToHandler = this.targetToHandlersByName.apply(target);
Map<Method, MethodHandler> methodToHandler = new LinkedHashMap();
List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList();
Method[] var5 = target.type().getMethods();
int var6 = var5.length;
for(int var7 = 0; var7 < var6; ++var7) {
Method method = var5[var7];
if (method.getDeclaringClass() != Object.class) {
if (Util.isDefault(method)) {
DefaultMethodHandler handler = new DefaultMethodHandler(method);
defaultMethodHandlers.add(handler);
methodToHandler.put(method, handler);
} else {
methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
}
}
}
InvocationHandler handler = this.factory.create(target, methodToHandler);
//创建代理对象
T proxy = Proxy.newProxyInstance(target.type().getClassLoader(), new Class[]{
target.type()}, handler);
Iterator var12 = defaultMethodHandlers.iterator();
while(var12.hasNext()) {
DefaultMethodHandler defaultMethodHandler = (DefaultMethodHandler)var12.next();
defaultMethodHandler.bindTo(proxy);
}
return proxy;
}
}
ReflectiveFeign 类中的 invoke 方法帮我们完成调用:
当我们去进行远程调用的时候可以 debug 到对应的断点:
SynchronousMethodHandler 的 invoke 中给每一个请求创建了一个 requestTemplate 对 象,去执行请求:
final class SynchronousMethodHandler implements MethodHandler {
//执行请求走对应的invoke方法
public Object invoke(Object[] argv) throws Throwable {
//可以看到构建了一个RequestTemplate
RequestTemplate template = this.buildTemplateFromArgs.create(argv);
Options options = this.findOptions(argv);
Retryer retryer = this.retryer.clone();
while(true) {
try {
//交给ribbon来进行请求调用
return this.executeAndDecode(template, options);
} catch (RetryableException var9) {
...
}
}
}
}
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
Request request = this.targetRequest(template);
if (this.logLevel != Level.NONE) {
this.logger.logRequest(this.metadata.configKey(), this.logLevel, request);
}
long start = System.nanoTime();
Response response;
try {
//这里client指的是LoadBalancerFeignClient,就是对应的负载均衡客户端请求工具
response = this.client.execute(request, options);
response = response.toBuilder().request(request).requestTemplate(template).build();
} catch (IOException var12) {
if (this.logLevel != Level.NONE) {
this.logger.logIOException(this.metadata.configKey(), this.logLevel, var12, this.elapsedTime(start));
}
throw FeignException.errorExecuting(request, var12);
}
}
此时就会走 LoadBalancerFeignClient 中的 execute():
public class LoadBalancerFeignClient implements Client {
public Response execute(Request request, Options options) throws IOException {
try {
//整理得到对应的url
URI asUri = URI.create(request.url());
String clientName = asUri.getHost();
URI uriWithoutHost = cleanUrl(request.url(), clientName);
RibbonRequest ribbonRequest = new RibbonRequest(this.delegate, request, uriWithoutHost);
IClientConfig requestConfig = this.getClientConfig(options, clientName);
//执行处理根据负载均衡处理器
return ((RibbonResponse)this.lbClient(clientName).executeWithLoadBalancer(ribbonRequest, requestConfig)).toResponse();
} catch (ClientException var8) {
IOException io = this.findIOException(var8);
if (io != null) {
throw io;
} else {
throw new RuntimeException(var8);
}
}
}
}
接着会走 AbstractLoadBalancerAwareClient 的 executeWithLoadBalancer:
public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig);
try {
return command.submit(
new ServerOperation<T>() {
//此时Server对象会取得最终的ip地址,例如:ip地址:port
@Override
public Observable<T> call(Server server) {
URI finalUri = reconstructURIWithServer(server, request.getUri());
S requestForServer = (S) request.replaceUri(finalUri);
try {
//这里才是真正进行发送请求操作
return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
}
catch (Exception e) {
return Observable.error(e);
}
}
})
.toBlocking()
.single();
} catch (Exception e) {
Throwable t = e.getCause();
if (t instanceof ClientException) {
throw (ClientException) t;
} else {
throw new ClientException(e);
}
}
}
最终在 FeignLoadBalancer 中的 execute 方法中完成远程方法访问:
public FeignLoadBalancer.RibbonResponse execute(FeignLoadBalancer.RibbonRequest request, IClientConfig configOverride) throws IOException {
Options options;
if (configOverride != null) {
RibbonProperties override = RibbonProperties.from(configOverride);
options = new Options((long)override.connectTimeout(this.connectTimeout), TimeUnit.MILLISECONDS, (long)override.readTimeout(this.readTimeout), TimeUnit.MILLISECONDS, override.isFollowRedirects(this.followRedirects));
} else {
options = new Options((long)this.connectTimeout, TimeUnit.MILLISECONDS, (long)this.readTimeout, TimeUnit.MILLISECONDS, this.followRedirects);
}
//最终这里完成了远程方法调用
Response response = request.client().execute(request.toRequest(), options);
return new FeignLoadBalancer.RibbonResponse(request.getUri(), response);
}
其实本质就是发送的 HttpURLConnection
public Response execute(Request request, Options options) throws IOException {
//发起HttpURLConnection请求
HttpURLConnection connection = this.convertAndSend(request, options);
//转转响应体
return this.convertResponse(connection, request);
}
Response convertResponse(HttpURLConnection connection, Request request) throws IOException {
//获取到状态码
int status = connection.getResponseCode();
String reason = connection.getResponseMessage();
if (status < 0) {
throw new IOException(String.format("Invalid status(%s) executing %s %s", status, connection.getRequestMethod(), connection.getURL()));
} else {
Map<String, Collection<String>> headers = new LinkedHashMap();
Iterator var6 = connection.getHeaderFields().entrySet().iterator();
while(var6.hasNext()) {
Entry<String, List<String>> field = (Entry)var6.next();
if (field.getKey() != null) {
headers.put(field.getKey(), field.getValue());
}
}
Integer length = connection.getContentLength();
if (length == -1) {
length = null;
}
InputStream stream;
if (status >= 400) {
stream = connection.getErrorStream();
} else {
stream = connection.getInputStream();
}
return Response.builder().status(status).reason(reason).headers(headers).request(request).body(stream, length).build();
}
}
4.4、feign 调用问题快速定位
只要是 feign 调用出了问题,看 feign 包下面的 Client 接口下面的 108 行。
我们可以根据对应响应的状态码来进行定位:
200:成功 400:请求参数错误 401:没有权限
403:权限不够 404:路径不匹配 405:方法不允许
500:提供者报错了 302:资源重定向
OpenFeign 主要基于接口和注解实现了远程调用。
源码总结:面试
1、OpenFeign 用过吗?它是如何运作的?
在主启动类上加上@EnableFeignClients 注解后,启动会进行包扫描,把所有加了 @FeignClient(value=”xxx-service”)注解的接口进行创建代理对象通过代理对象,使用 ribbon 做了负载均衡和远程调用
2、如何创建的代理对象?
当 项 目 在 启 动 时 , 先 扫 描 , 然 后 拿 到 标 记 了 @FeignClient 注 解 的 接 口 信 息 , 由 ReflectiveFeign 类的 newInstance 方法创建了代理对象 JDK 代理
3、OpenFeign 到底是用什么做的远程调用?
使用的是 HttpURLConnection (java.net)
- OpenFeign 怎么和 ribbon 整合的?
在代理对象执行调用的时候