深入浅出Spring Cloud整合OpenFeign

参考

参考1

参考2

Spring Cloud OpenFeign 3.0.1

Spring Cloud 版本 <spring-cloud.version>2020.0.1</spring-cloud.version>

案例说明
在这里插入图片描述
8951是消费者,服务消费方,调用方
8952-user 是生产者,服务提供方,被调用方

服务提供者,被调用方 8952-user

完整依赖如下

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>

作为被调用方需要将自己注册到注册中心,这里以nacos为例。项目结构图如下所示。

在这里插入图片描述

bootstrap.yml

spring:
  application:
    name: openfeign-8952-user
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
server:
  port: 8952

UserController 随便写几个测试方法

@RestController
public class UserController {

    private final static Logger logger = LoggerFactory.getLogger(UserController.class);

    @Value(value = "${server.port}")
    private String port;

    @GetMapping("user")

    public String getUser() {
        return "user" + port;
    }

    @GetMapping("user2")
    public String getUser2(Integer id) {
        return "this is user2:" + id + port;
    }

    @PostMapping("user3")
    public String getUser3(@RequestBody Integer id) {
        return "this is user3:" + id + port;
    }

    @PostMapping("user4")
    public String getUser4(@RequestBody Integer id, HttpServletRequest request) {
        logger.info("header {}", request.getHeader("token"));
        return "this is user3:" + id + port;
    }

    @PostMapping("user5")
    public String getUser5() {
        try {
            TimeUnit.SECONDS.sleep(11);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "ok...";

    }
}

作为生产者,和任何其他单体应用的区别是把自己注册到注册中心而已。

服务消费者,调用方 8951

完整依赖如下

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>

    </dependencies>

完整结构图
在这里插入图片描述
UserClient

@FeignClient("openfeign-8952-user")
public interface UserClient {

    @GetMapping("user")
    String getUser();

    @GetMapping("user2")
    String getUser2(@RequestParam(value = "id") Integer id);

    @PostMapping(value = "user3", headers = "{}")
    String getUser3(@RequestBody Integer id);

    @PostMapping("user4")
    String getUser4(@RequestBody Integer id, @RequestHeader String token);

    @PostMapping("user5")
    String getUser5();

}

UserController

@RestController
public class UserController {

    @Autowired
    UserClient userClient;

    @GetMapping("user")
    public String getUser() {
        return userClient.getUser();
    }

    @GetMapping("user2")
    public String getUser2(Integer id) {
        return userClient.getUser2(id);
    }

    @GetMapping("user3")
    public String getUser3(Integer id) {
        return userClient.getUser3(id);
    }

    @GetMapping("user4")
    public String getUser4(Integer id) {
        String token = UUID.randomUUID().toString();
        return userClient.getUser4(id, token);
    }

    @GetMapping("user5")
    public String getUser5() {
        return userClient.getUser5();
    }
}

nacos服务列表如下
在这里插入图片描述
访问 http://localhost:8951/user 成功返回 user8952

注意事项

在比较新的版本(如3.0.4)中如果没有添加 spring-cloud-starter-loadbalancer 会报错,如下图所示。

在这里插入图片描述
后面我们会分析报错的一个原理。

负载均衡

快速启动另一台服务,右键选择复制配置
在这里插入图片描述
在程序参数指定另一个端口

--server.port=8953

在这里插入图片描述
启动服务并查看注册中心
在这里插入图片描述

在这里插入图片描述
访问 8951/user
在这里插入图片描述
可以看到已经具有了负载均衡的能力。

loadbalancer 依赖与启动分析

我们删除 spring-cloud-starter-loadbalancer 依赖根据报错信息找到对应的方法。
在这里插入图片描述
从上面截图第三行这个loadBalance方法点进去,源码如下所示

	protected <T> T loadBalance(Feign.Builder builder, FeignContext context, HardCodedTarget<T> target) {
		Client client = getOptional(context, Client.class);
		if (client != null) {
			builder.client(client);
			Targeter targeter = get(context, Targeter.class);
			return targeter.target(this, builder, context, target);
		}
		//如果 client == null
		throw new IllegalStateException(
				"No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-loadbalancer?");
	}

可以看到 throw 后面的内容就是报错信息里面的提示。也就是说client == null的时候会抛出这个异常。

那么 Client client = getOptional(context, Client.class); 这一行就是重点。点进去看一下实现。

	protected <T> T getOptional(FeignContext context, Class<T> type) {
		return context.getInstance(contextId, type);
	}

调用了context.getInstance继续点进去。

	public <T> T getInstance(String name, Class<T> type) {
		AnnotationConfigApplicationContext context = getContext(name);
		try {
			return context.getBean(type);
		}
		catch (NoSuchBeanDefinitionException e) {
			// ignore
		}
		return null;
	}

这里是从容器AnnotationConfigApplicationContext中获取到bean,那么谁创建的这个bean呢?

一般来说都是由AutoConfiguration默认配置,所以我们找到loadbalancer包下的AutoConfiguration类。

在这里插入图片描述

FeignLoadBalancerAutoConfiguration 部分源码如下所示。

@ConditionalOnClass(Feign.class)
@ConditionalOnBean({ LoadBalancerClient.class, LoadBalancerClientFactory.class })
@AutoConfigureBefore(FeignAutoConfiguration.class)
@AutoConfigureAfter({ BlockingLoadBalancerClientAutoConfiguration.class, LoadBalancerAutoConfiguration.class })
@EnableConfigurationProperties(FeignHttpClientProperties.class)
@Configuration(proxyBeanMethods = false)
@Import({ HttpClientFeignLoadBalancerConfiguration.class, OkHttpFeignLoadBalancerConfiguration.class,
		DefaultFeignLoadBalancerConfiguration.class })
public class FeignLoadBalancerAutoConfiguration {}

通过import将三个客户端的配置类加入容器,由于我们还未做客户端切换所以找到Default。

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(LoadBalancerProperties.class)
class DefaultFeignLoadBalancerConfiguration {

	@Bean
	@ConditionalOnMissingBean
	@Conditional(OnRetryNotEnabledCondition.class)
	public Client feignClient(LoadBalancerClient loadBalancerClient, LoadBalancerProperties properties,
			LoadBalancerClientFactory loadBalancerClientFactory) {
		return new FeignBlockingLoadBalancerClient(new Client.Default(null, null), loadBalancerClient, properties,
				loadBalancerClientFactory);
	}

	@Bean
	@ConditionalOnMissingBean
	@ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")
	@ConditionalOnBean(LoadBalancedRetryFactory.class)
	@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true",
			matchIfMissing = true)
	public Client feignRetryClient(LoadBalancerClient loadBalancerClient,
			LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties,
			LoadBalancerClientFactory loadBalancerClientFactory) {
		return new RetryableFeignBlockingLoadBalancerClient(new Client.Default(null, null), loadBalancerClient,
				loadBalancedRetryFactory, properties, loadBalancerClientFactory);
	}

}

所以容器中获取到的Client类型的bean就是FeignBlockingLoadBalancerClient。如何验证?

我们回到loadBalance这个方法打一个断点看一下获取到的client类型就可以了。

在这里插入图片描述
删掉依赖后查看 FeignLoadBalancerAutoConfiguration 类情况
在这里插入图片描述
在ConditionalOnBean的作用下,这个AutoConf 不会执行就会导致容器中没有对应的 Client 。

到此为止,依赖不添加为什么报错已经说清楚了,那么获取到客户端负载均衡的过程呢?

负载均衡原理

在上面我们已经获取到了这么一个客户端 FeignBlockingLoadBalancerClient。里面有一个execute方法。

	@Override
	public Response execute(Request request, Request.Options options) throws IOException {}

猜想:在请求接口的时候会进到这里面。

验证猜想:在 loadBalancerClient.choose(serviceId, lbRequest); 这一行打一个断点观察输出

方法调用栈过程如下图
在这里插入图片描述
经过了动态代理的invoke,最终进到上面刚刚获取的FeignBlockingLoadBalancerClient的execute里面。

		ServiceInstance instance = loadBalancerClient.choose(serviceId, lbRequest);

最终通过这一行来筛选出负载均衡后的服务,我们重点看一下里面的实现。

点进去来到 BlockingLoadBalancerClient类中的choose方法。简称为阻塞的负载均衡客户端

	@Override
	public <T> ServiceInstance choose(String serviceId, Request<T> request) {
		ReactiveLoadBalancer<ServiceInstance> loadBalancer = loadBalancerClientFactory.getInstance(serviceId);
		if (loadBalancer == null) {
			return null;
		}
		Response<ServiceInstance> loadBalancerResponse = Mono.from(loadBalancer.choose(request)).block();
		if (loadBalancerResponse == null) {
			return null;
		}
		return loadBalancerResponse.getServer();
	}

这个类是由 BlockingLoadBalancerClientAutoConfiguration 这个类装配而来。

@Configuration(proxyBeanMethods = false)
@LoadBalancerClients
@AutoConfigureAfter(LoadBalancerAutoConfiguration.class)
@AutoConfigureBefore({ org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration.class,
		AsyncLoadBalancerAutoConfiguration.class })
@ConditionalOnClass(RestTemplate.class)
public class BlockingLoadBalancerClientAutoConfiguration {

	@Bean
	@ConditionalOnBean(LoadBalancerClientFactory.class)
	@ConditionalOnMissingBean
	public LoadBalancerClient blockingLoadBalancerClient(LoadBalancerClientFactory loadBalancerClientFactory,
			LoadBalancerProperties properties) {
		return new BlockingLoadBalancerClient(loadBalancerClientFactory, properties);
	}
	... }

choose方法主要分为两段,第一段获得 ReactiveLoadBalancer 这么一个类型的变量。
第二段获得 Response 这么一个类型的返回值,里面包含了服务器相关信息。

Response<ServiceInstance> loadBalancerResponse = Mono.from(loadBalancer.choose(request)).block();
loadBalancer.choose(request)

在这里插入图片描述
在这里插入图片描述

@Configuration(proxyBeanMethods = false)
@ConditionalOnDiscoveryEnabled
public class LoadBalancerClientConfiguration {

	private static final int REACTIVE_SERVICE_INSTANCE_SUPPLIER_ORDER = 193827465;

	@Bean
	@ConditionalOnMissingBean
	public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(Environment environment,
			LoadBalancerClientFactory loadBalancerClientFactory) {
		String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
		return new RoundRobinLoadBalancer(
				loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
	}

RoundRobinLoadBalancer 中核心方法

	public Mono<Response<ServiceInstance>> choose(Request request) {
		ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
				.getIfAvailable(NoopServiceInstanceListSupplier::new);
		return supplier.get(request).next()
				.map(serviceInstances -> processInstanceResponse(supplier, serviceInstances));
	}

	private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier,
			List<ServiceInstance> serviceInstances) {
		Response<ServiceInstance> serviceInstanceResponse = getInstanceResponse(serviceInstances);
		if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
			((SelectedInstanceCallback) supplier).selectedServiceInstance(serviceInstanceResponse.getServer());
		}
		return serviceInstanceResponse;
	}

	private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
		if (instances.isEmpty()) {
			if (log.isWarnEnabled()) {
				log.warn("No servers available for service: " + serviceId);
			}
			return new EmptyResponse();
		}
		// TODO: enforce order?
		int pos = Math.abs(this.position.incrementAndGet());

		ServiceInstance instance = instances.get(pos % instances.size());

		return new DefaultResponse(instance);
	}

Timeout Handling (超时处理)

说明:2020版本之前,超时控制由 ribbon 完成 。

spring-cloud-starter-openfeign 查看 pom 有无 ribbon来选择不同的配置方式。

旧版 ribbon 配置代码如下所示。

ribbon:
  #指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
  ReadTimeout: 5000
  #指的是建立连接后从服务器读取到可用资源所用的时间
  ConnectTimeout: 5000

新版 FeignClientProperties

在这里插入图片描述
FeignClientConfiguration 类关键参数如下图
在这里插入图片描述
配置文件写入下面这段内容

feign:
  client:
    config:
      openfeign-8952-user:
        connectTimeout: 1000
        readTimeout: 1000

打一个断点来看是否写成功
在这里插入图片描述
调用 user5 方法,可以看到成功相应超时设置的 1s
在这里插入图片描述

更换http客户端

快速上手

使用 httpclient

第一步:添加依赖

<!-- https://mvnrepository.com/artifact/io.github.openfeign/feign-httpclient -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
    <version>11.10</version>
</dependency>

第二步验证:是否生效
在这里插入图片描述

使用 okhttp

第一步:添加依赖

<!-- https://mvnrepository.com/artifact/io.github.openfeign/feign-okhttp -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
    <version>11.10</version>
</dependency>

第二步:配置文件修改

feign:
  okhttp:
    enabled: true
  httpclient:
    enabled: false

第三步验证:是否生效
在这里插入图片描述

Tips:如果同时有两种依赖,记得将 httpclient enabled 设置为 false,否则会先加载。

如何看使用的什么客户端

在 FeignBlockingLoadBalancerClient 构造函数这个地方打一个断点观察 delegate 参数
在这里插入图片描述

性能比较

HTTP 连接客户端,选 HttpClient 还是 OkHttp ?

总结: OkHttp和HttpClient在性能和使用上不分伯仲,根据实际业务选择即可。

日志

1.10. Feign logging

log level 选择

public enum LogLevel {
	TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF
}
logging:
  level:
    com.example.openfeign8951.client.UserClient: debug

新增配置类

@Configuration
public class FeignLogConfig {

    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

Level主要包含以下级别:NONE,BASIC,HEADERS,FULL。

重试

FeignClient配置

FeignAutoConfiguration中的EnableConfigurationProperties让配置文件生效

@EnableConfigurationProperties({ FeignClientProperties.class, FeignHttpClientProperties.class })

FeignClientProperties

FeignClientProperties中的FeignClientConfiguration控制

	public static class FeignClientConfiguration {
		//日志级别
		private Logger.Level loggerLevel;
		//连接超时
		private Integer connectTimeout;
		//读超时
		private Integer readTimeout;
		//重试
		private Class<Retryer> retryer;
		//错误码
		private Class<ErrorDecoder> errorDecoder;
		//过滤器
		private List<Class<RequestInterceptor>> requestInterceptors;
		//默认请求头
		private Map<String, Collection<String>> defaultRequestHeaders;
		//默认查询参数
		private Map<String, Collection<String>> defaultQueryParameters;
		//是否开启404
		private Boolean decode404;
		//decoder
		private Class<Decoder> decoder;
		//encoder
		private Class<Encoder> encoder;
		//Contract
		private Class<Contract> contract;
		//异常传播策略
		private ExceptionPropagationPolicy exceptionPropagationPolicy;
	}

decode404

在上面超时控制的时候我们配置了 FeignClientProperties 这个类里面包含一个参数decode404

在这里插入图片描述
我们来看一下true和false的区别。

feign:
  client:
    config:
      openfeign-8952-user:
        connectTimeout: 1000
        readTimeout: 1000
        decode404: false

decode404: true

{"timestamp":"2022-11-10T09:33:25.917+00:00","status":404,"error":"Not Found","message":"","path":"/aaa"}

decode404: false

feign.FeignException$NotFound: [404 ] during [GET] to [http://openfeign-8952-user/aaa] [UserClient#aaa()]: [{"timestamp":"***","status":404,"error":"Not Found","message":"","path":"/aaa"}]

不开启会得到一个错误页面,开启会得到一个相对友好的提示。

当我们学习到FeignClient这个注解的时候发现里面也有这样一个字段 boolean decode404() default false;

探究 FeignClient中decode404和FeignClientConfiguration中decode404的关联

猜想:注解上的404和配置文件404只要有一个为true则为true。

首先将注解的 decode404 和 配置文件的 decode404 都设为 false,控制台输出如下所示。

在这里插入图片描述
查找那里调用了 FeignClient
在这里插入图片描述
FeignClientsRegistrar 这个类是由 EnableFeignClients 注解 @Import(FeignClientsRegistrar.class) 导入的

在这里插入图片描述
在 getAnnotationAttributes 方法后打一个断点可以看到 Map<String, Object> attributes 存储了注解上定义的相关信息。
在这里插入图片描述
我们接下来看一下这个注解获取到的404对象做了什么?
在这里插入图片描述
factoryBean对象将注解中的404值拿过来了
在这里插入图片描述
接下来的调用过程如图所示
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
如果是默认的配置文件使用configure进行配置,否则使用配置文件配置。

//configureUsingConfiguration
		if (decode404) {
			builder.decode404();
		}
//configureUsingProperties
		if (config.getDecode404() != null) {
			if (config.getDecode404()) {
				builder.decode404();
			}
		}

所以我们得出结论,二者有true为true不会覆盖,相当于 或 的关系。
验证猜想
在两个 builder.decode404() 处打断点,然后分别将 注解 和 配置文件 设置为 true,看是否会跑到断点。

注解为true
在这里插入图片描述
配置文件为true
在这里插入图片描述

FeignHttpClientProperties

降级 (fallback)

1.5. Feign Spring Cloud CircuitBreaker Support

准备工作

1:添加依赖

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
            <version>2.2.10.RELEASE</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-circuitbreaker-resilience4j -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
        </dependency>

两个依赖添加任何一个都可以,openfeign在高版本移出了默认的hystrix。

在openfeign低版本中会默认添加feign-hystrix,2.2.10依赖如下图所示

在这里插入图片描述
3.0.0版本如下图所示

在这里插入图片描述

快速上手:fallback

1.6. Feign Spring Cloud CircuitBreaker Fallbacks

1:开启熔断配置

feign:
  circuitbreaker:
    enabled: true

2:在被调用方开发一个错误接口

    @GetMapping("bbb")
    public void bbb() {
        int a = 1 / 0;
    }

3:参照官方文档实现接口

在这里插入图片描述
新建一个UserClient 实现类并被Spring容器接管。

@Component
public class UserClientFallback implements UserClient {

    public static final String DEFAULT = "default:";

    @Override
    public String bbb() {
        return DEFAULT;
    }
}

4:在注解上指定UserClientFallback类

@FeignClient(value = "openfeign-8952-user", fallback = UserClientFallback.class)

快速上手:fallbackFactory

官方示例,基本关系如图所示
在这里插入图片描述
If one needs access to the cause that made the fallback trigger, one can use the fallbackFactory attribute inside @FeignClient. 如果需要访问产生回退触发器的原因,你可以使用fallbackFactory这个属性。

	T create(Throwable cause);

创建工厂类UserClientFactory ,一般统一处理业务异常和超时异常。

@Component
public class UserClientFactory implements FallbackFactory<UserClient> {

    private static final Logger logger = LoggerFactory.getLogger(UserClientFactory.class);

    @Override
    public UserClient create(Throwable cause) {
        throw handleThrowable(cause);
    }

    /**
     * 处理feign中异常
     *
     * @param cause
     * @return
     */
    public RuntimeException handleThrowable(Throwable cause) {
        //TODO自定义异常解析
        if (cause instanceof TimeoutException) {
            return new RuntimeException("服务繁忙,请稍后重试");
        } else {

        }
        return (RuntimeException) cause;
    }

}

快速上手:优化

@FeignClient 和 @EnableFeignClients

@EnableFeignClients 的作用是什么?
在这里插入图片描述
可以看到这个注解的核心就是 @Import(FeignClientsRegistrar.class) 这一行,那么他有什么作用呢?

定义一个 User 对象

public class User {

    String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public User() {
        this.name = "default";
    }

    public User(String name) {
        this.name = name;
    }
}

主启动类@Import(User.class)

在这里插入图片描述

注入对象

    @Autowired
    User user;
    
    @GetMapping("user6")
    public String getUser6() {
        return user.getName();
    }

发现输出了 default,代表走的无参构造函数。

图解+源码讲解代理对象 ReflectiveFeign 分析

这里可以看到userClient是一个代理对象。
在这里插入图片描述
我们注入的接口 UserClient 是一个代理对象,触发了动态代理的invoke方法
在这里插入图片描述

FeignAutoConfiguration

FeignClientsRegistrar

FeignClientFactoryBean

FeignClientsRegistrar中有一个registerFeignClient方法调用了 return factoryBean.getObject();

	@Override
	public Object getObject() {
		return getTarget();
	}

getTarget方法源码

	<T> T getTarget() {
	//根据beanFactory是否为null从上下文或者beanFactory中获取FeignContext
		FeignContext context = beanFactory != null ? beanFactory.getBean(FeignContext.class)
				: applicationContext.getBean(FeignContext.class);
		//配置
		Feign.Builder builder = feign(context);

		if (!StringUtils.hasText(url)) {
			if (!name.startsWith("http")) {
				url = "http://" + name;
			}
			else {
				url = name;
			}
			url += cleanPath();
			return (T) loadBalance(builder, context, new HardCodedTarget<>(type, name, url));
		}
		if (StringUtils.hasText(url) && !url.startsWith("http")) {
			url = "http://" + url;
		}
		String url = this.url + cleanPath();
		Client client = getOptional(context, Client.class);
		if (client != null) {
			if (client instanceof FeignBlockingLoadBalancerClient) {
				// not load balancing because we have a url,
				// but Spring Cloud LoadBalancer is on the classpath, so unwrap
				client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
			}
			builder.client(client);
		}
		//从上下文中拿Targeter 
		Targeter targeter = get(context, Targeter.class);
		return (T) targeter.target(this, builder, context, new HardCodedTarget<>(type, name, url));
	}

Targeter 有两个子类

在这里插入图片描述
其中DefaultTargeter是FeignAutoConfiguration这两个地方配置的。

当我们FeignAutoConfiguration中将feign.circuitbreaker.enabled设为true的时候,会配置下面两个bean

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnClass(CircuitBreaker.class)
	@ConditionalOnProperty("feign.circuitbreaker.enabled")
	protected static class CircuitBreakerPresentFeignTargeterConfiguration {

		@Bean
		@ConditionalOnMissingBean(CircuitBreakerFactory.class)
		public Targeter defaultFeignTargeter() {
			return new DefaultTargeter();
		}

		@Bean
		@ConditionalOnMissingBean
		@ConditionalOnBean(CircuitBreakerFactory.class)
		public Targeter circuitBreakerFeignTargeter(CircuitBreakerFactory circuitBreakerFactory) {
			return new FeignCircuitBreakerTargeter(circuitBreakerFactory);
		}
	}

CircuitBreakerFactory主要实现类如下图
在这里插入图片描述

降级失败原理

Feign类中的newInstance
在这里插入图片描述
我们知道代理类执行方法的时候会调用InvocationHandler,所以我们分别看一下两个实现。

FeignCircuitBreakerInvocationHandler

	@Override
	public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
		//省略部分代码
		String circuitName = this.feignClientName + "_" + method.getName();
		CircuitBreaker circuitBreaker = this.factory.create(circuitName);
		Supplier<Object> supplier = asSupplier(method, args);
		if (this.nullableFallbackFactory != null) {
			Function<Throwable, Object> fallbackFunction = throwable -> {
				Object fallback = this.nullableFallbackFactory.create(throwable);
				try {
					return this.fallbackMethodMap.get(method).invoke(fallback, args);
				}
				catch (Exception e) {
					throw new IllegalStateException(e);
				}
			};
			return circuitBreaker.run(supplier, fallbackFunction);
		}
		return circuitBreaker.run(supplier);
	}

FeignInvocationHandler

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
	//省略部分代码
      return dispatch.get(method).invoke(args);
    }

到这里应该就明白为什么不生效了,当创建defaultFeignTargeter的时候没有对方法进行任何增强处理,自然不具备降级的能力。

Resilience4JAutoConfiguration

Resilience4JAutoConfiguration中部分代码

@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = { "spring.cloud.circuitbreaker.resilience4j.enabled",
		"spring.cloud.circuitbreaker.resilience4j.blocking.enabled" }, matchIfMissing = true)
public class Resilience4JAutoConfiguration {

	@Autowired(required = false)
	private List<Customizer<Resilience4JCircuitBreakerFactory>> customizers = new ArrayList<>();

	@Bean
	@ConditionalOnMissingBean(CircuitBreakerFactory.class)
	public Resilience4JCircuitBreakerFactory resilience4jCircuitBreakerFactory() {
		Resilience4JCircuitBreakerFactory factory = new Resilience4JCircuitBreakerFactory();
		customizers.forEach(customizer -> customizer.customize(factory));
		return factory;
	}
}

扩展

深入Feign源码吃透Spring扩展点「扩展点实战系列」- 第446篇

Feign 中的自动装配

第一步:找到 spring.factories 这个文件
在这里插入图片描述

一共有 5 个自动配置类

FeignHalAutoConfiguration
FeignAutoConfiguration
FeignAcceptGzipEncodingAutoConfiguration
FeignContentGzipEncodingAutoConfiguration
FeignLoadBalancerAutoConfiguration

先后加载关系

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
FeignLoadBalancer 先于 Feign ,FeignContentGzip 和 FeignAcceptGzip 在 Feign 之后加载。

FeignLoadBalancerAutoConfiguration

@Import({ HttpClientFeignLoadBalancerConfiguration.class, OkHttpFeignLoadBalancerConfiguration.class,
		DefaultFeignLoadBalancerConfiguration.class })
public class FeignLoadBalancerAutoConfiguration {

}

核心是 Import 导入的三个配置类

HttpClientFeignLoadBalancerConfiguration

条件注解

@ConditionalOnClass(ApacheHttpClient.class)
@ConditionalOnBean({ LoadBalancerClient.class, LoadBalancerClientFactory.class })
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
  • ApacheHttpClient 这个类存在为 true 。
  • LoadBalancerClient 和 LoadBalancerClientFactory Bean 在容器内才为 true。
  • 当这个注解 matchIfMissing = true 的时候,配置文件没有写也为 true。

当所有条件为 true 的时候,执行配置类。

	@Bean
	@ConditionalOnMissingBean
	@Conditional(OnRetryNotEnabledCondition.class)
	public Client feignClient(LoadBalancerClient loadBalancerClient, HttpClient httpClient,
			LoadBalancerProperties properties, LoadBalancerClientFactory loadBalancerClientFactory) {
		ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
		return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient, properties, loadBalancerClientFactory);
	}

所以当我们将 feign-httpclient 注入的时候,容器中存在的 Client 类型为 FeignBlockingLoadBalancerClient

        Object feignClient = run.getBean("feignClient");
        System.out.println(feignClient);

如何验证? 在主启动类获取名为 feignClient 的 Bean 。

org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient@3d3c886f
  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Cloud是一个为开发者提供了快速构建分布式系统的工具集,其中非常重要的一部分就是OpenFeignOpenFeign是一个声明式、模板化的HTTP客户端,它可以让开发者更加方便的调用HTTP接口。下面我们来详细了解一下Spring Cloud整合OpenFeign的使用方式。 首先,我们需要在pom.xml文件中添加依赖,如下所示: ``` <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> <version>{版本号}</version> </dependency> ``` 然后,我们需要在启动类上添加@EnableFeignClients注解,表示开启Feign客户端自动配置。同时,我们还需要通过@FeignClient注解来定义接口。例如: ``` @FeignClient(name = "user-service") public interface UserFeignClient { @GetMapping("/user/findById") User findById(@RequestParam("id") Long id); } ``` 在上面的代码中,@FeignClient注解中的name属性表示调用的服务名,而接口中的findById方法就是定义的远程调用的接口。其中,@RequestParam注解表示使用@RequestParam方式传参。 最后,我们在业务代码中使用定义的接口即可。例如: ``` @RestController public class UserController { @Autowired private UserFeignClient userFeignClient; @GetMapping("/findUser") public User findUser(Long id) { return userFeignClient.findById(id); } } ``` 通过以上步骤,我们就可以方便地使用OpenFeign来调用HTTP接口,实现微服务之间的远程调用。整合OpenFeign有很多细节需要注意,例如如何处理调用异常、如何配置重试等等。但总体来说,Spring Cloud整合OpenFeign使用起来非常简单,是我们构建分布式系统的重要利器之一。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值